瀏覽代碼

Merge branch 'master' into comparisons

Cody Robibero 3 年之前
父節點
當前提交
634ce40c2f
共有 100 個文件被更改,包括 2289 次插入2690 次删除
  1. 1 1
      .ci/azure-pipelines-abi.yml
  2. 8 1
      .ci/azure-pipelines-main.yml
  3. 25 3
      .ci/azure-pipelines-package.yml
  4. 2 2
      .ci/azure-pipelines-test.yml
  5. 3 2
      .ci/azure-pipelines.yml
  6. 1 0
      .copr
  7. 0 1
      .copr/Makefile
  8. 0 49
      .github/ISSUE_TEMPLATE/bug_report.md
  9. 106 0
      .github/ISSUE_TEMPLATE/issue report.yml
  10. 5 1
      .github/stale.yml
  11. 7 5
      .github/workflows/automation.yml
  12. 2 1
      .github/workflows/codeql-analysis.yml
  13. 1 1
      .github/workflows/commands.yml
  14. 124 0
      .github/workflows/openapi.yml
  15. 3 0
      .gitignore
  16. 2 2
      .vscode/launch.json
  17. 5 0
      CONTRIBUTORS.md
  18. 17 0
      Directory.Build.props
  19. 31 19
      Dockerfile
  20. 26 19
      Dockerfile.arm
  21. 26 17
      Dockerfile.arm64
  22. 3 2
      DvdLib/DvdLib.csproj
  23. 2 1
      DvdLib/Ifo/Dvd.cs
  24. 1 3
      Emby.Dlna/Configuration/DlnaOptions.cs
  25. 1 3
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  26. 251 584
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  27. 11 8
      Emby.Dlna/ContentDirectory/ServerItem.cs
  28. 3 3
      Emby.Dlna/ControlResponse.cs
  29. 15 17
      Emby.Dlna/Didl/DidlBuilder.cs
  30. 56 57
      Emby.Dlna/DlnaManager.cs
  31. 2 8
      Emby.Dlna/Emby.Dlna.csproj
  32. 3 3
      Emby.Dlna/EventSubscriptionResponse.cs
  33. 7 22
      Emby.Dlna/Eventing/DlnaEventManager.cs
  34. 12 23
      Emby.Dlna/Main/DlnaEntryPoint.cs
  35. 9 24
      Emby.Dlna/PlayTo/Device.cs
  36. 7 3
      Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
  37. 3 5
      Emby.Dlna/PlayTo/PlayToController.cs
  38. 4 5
      Emby.Dlna/PlayTo/PlayToManager.cs
  39. 5 2
      Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
  40. 5 2
      Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
  41. 5 2
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  42. 9 7
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  43. 3 5
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  44. 3 7
      Emby.Dlna/Service/BaseControlHandler.cs
  45. 4 4
      Emby.Dlna/Service/BaseService.cs
  46. 1 5
      Emby.Dlna/Service/ControlErrorHandler.cs
  47. 1 7
      Emby.Drawing/Emby.Drawing.csproj
  48. 2 2
      Emby.Drawing/ImageProcessor.cs
  49. 1 1
      Emby.Naming/Audio/AudioFileParser.cs
  50. 4 4
      Emby.Naming/AudioBook/AudioBookInfo.cs
  51. 6 6
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  52. 2 2
      Emby.Naming/AudioBook/AudioBookResolver.cs
  53. 75 21
      Emby.Naming/Common/NamingOptions.cs
  54. 3 9
      Emby.Naming/Emby.Naming.csproj
  55. 6 5
      Emby.Naming/Subtitles/SubtitleParser.cs
  56. 2 2
      Emby.Naming/TV/EpisodeResolver.cs
  57. 29 0
      Emby.Naming/TV/SeriesInfo.cs
  58. 60 0
      Emby.Naming/TV/SeriesPathParser.cs
  59. 19 0
      Emby.Naming/TV/SeriesPathParserResult.cs
  60. 49 0
      Emby.Naming/TV/SeriesResolver.cs
  61. 13 13
      Emby.Naming/Video/CleanStringParser.cs
  62. 76 30
      Emby.Naming/Video/ExtraResolver.cs
  63. 17 12
      Emby.Naming/Video/FileStack.cs
  64. 48 0
      Emby.Naming/Video/FileStackRule.cs
  65. 68 151
      Emby.Naming/Video/StackResolver.cs
  66. 2 2
      Emby.Naming/Video/StubResolver.cs
  67. 6 7
      Emby.Naming/Video/VideoInfo.cs
  68. 30 144
      Emby.Naming/Video/VideoListResolver.cs
  69. 8 7
      Emby.Naming/Video/VideoResolver.cs
  70. 1 5
      Emby.Notifications/Emby.Notifications.csproj
  71. 2 22
      Emby.Notifications/NotificationEntryPoint.cs
  72. 1 5
      Emby.Photos/Emby.Photos.csproj
  73. 2 1
      Emby.Photos/PhotoProvider.cs
  74. 1 1
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  75. 4 6
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  76. 236 379
      Emby.Server.Implementations/ApplicationHost.cs
  77. 1 112
      Emby.Server.Implementations/Archiving/ZipClient.cs
  78. 11 20
      Emby.Server.Implementations/Channels/ChannelManager.cs
  79. 2 1
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  80. 20 28
      Emby.Server.Implementations/Collections/CollectionManager.cs
  81. 52 74
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  82. 4 55
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  83. 4 2
      Emby.Server.Implementations/Data/ManagedConnection.cs
  84. 3 3
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  85. 399 309
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  86. 31 29
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  87. 30 0
      Emby.Server.Implementations/Data/SynchronouseMode.cs
  88. 23 0
      Emby.Server.Implementations/Data/TempStoreMode.cs
  89. 10 11
      Emby.Server.Implementations/Devices/DeviceId.cs
  90. 0 146
      Emby.Server.Implementations/Devices/DeviceManager.cs
  91. 46 72
      Emby.Server.Implementations/Dto/DtoService.cs
  92. 11 14
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  93. 1 7
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  94. 3 3
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  95. 3 0
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  96. 4 4
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  97. 14 7
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  98. 6 6
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  99. 6 1
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  100. 11 13
      Emby.Server.Implementations/IO/FileRefresher.cs

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

@@ -7,7 +7,7 @@ parameters:
   default: "ubuntu-latest"
   default: "ubuntu-latest"
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
   type: string
   type: string
-  default: 5.0.103
+  default: 6.0.x
 
 
 jobs:
 jobs:
   - job: CompatibilityCheck
   - job: CompatibilityCheck

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

@@ -1,7 +1,7 @@
 parameters:
 parameters:
   LinuxImage: 'ubuntu-latest'
   LinuxImage: 'ubuntu-latest'
   RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
   RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
-  DotNetSdkVersion: 5.0.103
+  DotNetSdkVersion: 6.0.x
 
 
 jobs:
 jobs:
   - job: Build
   - job: Build
@@ -91,3 +91,10 @@ jobs:
         inputs:
         inputs:
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
           targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
           artifactName: 'Jellyfin.Common'
           artifactName: 'Jellyfin.Common'
+
+      - task: PublishPipelineArtifact@1
+        displayName: 'Publish Artifact Extensions'
+        condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+        inputs:
+          targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
+          artifactName: 'Jellyfin.Extensions'

+ 25 - 3
.ci/azure-pipelines-package.yml

@@ -39,6 +39,14 @@ jobs:
     vmImage: 'ubuntu-latest'
     vmImage: 'ubuntu-latest'
 
 
   steps:
   steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+  - script: './bump-version $(JellyfinVersion)'
+    displayName: Bump internal version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
   - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
   - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
     displayName: 'Build Dockerfile'
     displayName: 'Build Dockerfile'
 
 
@@ -80,6 +88,14 @@ jobs:
     vmImage: 'ubuntu-latest'
     vmImage: 'ubuntu-latest'
 
 
   steps:
   steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+  - script: './bump-version $(JellyfinVersion)'
+    displayName: Bump internal version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
   - task: DownloadPipelineArtifact@2
   - task: DownloadPipelineArtifact@2
     displayName: 'Download OpenAPI Spec'
     displayName: 'Download OpenAPI Spec'
     inputs:
     inputs:
@@ -127,6 +143,10 @@ jobs:
     displayName: Set release version (stable)
     displayName: Set release version (stable)
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
 
+  - script: './bump-version $(JellyfinVersion)'
+    displayName: Bump internal version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
   - task: Docker@2
   - task: Docker@2
     displayName: 'Push Unstable Image'
     displayName: 'Push Unstable Image'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -181,7 +201,7 @@ jobs:
     inputs:
     inputs:
       sshEndpoint: repository
       sshEndpoint: repository
       runOptions: 'commands'
       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
 - job: PublishNuget
   displayName: 'Publish NuGet packages'
   displayName: 'Publish NuGet packages'
@@ -195,10 +215,10 @@ jobs:
 
 
   steps:
   steps:
   - task: UseDotNet@2
   - task: UseDotNet@2
-    displayName: 'Use .NET 5.0 sdk'
+    displayName: 'Use .NET 6.0 sdk'
     inputs:
     inputs:
       packageType: 'sdk'
       packageType: 'sdk'
-      version: '5.0.x'
+      version: '6.0.x'
 
 
   - task: DotNetCoreCLI@2
   - task: DotNetCoreCLI@2
     displayName: 'Build Stable Nuget packages'
     displayName: 'Build Stable Nuget packages'
@@ -211,6 +231,7 @@ jobs:
         MediaBrowser.Controller/MediaBrowser.Controller.csproj
         MediaBrowser.Controller/MediaBrowser.Controller.csproj
         MediaBrowser.Model/MediaBrowser.Model.csproj
         MediaBrowser.Model/MediaBrowser.Model.csproj
         Emby.Naming/Emby.Naming.csproj
         Emby.Naming/Emby.Naming.csproj
+        src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
       custom: 'pack'
       custom: 'pack'
       arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
       arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
 
 
@@ -225,6 +246,7 @@ jobs:
         MediaBrowser.Controller/MediaBrowser.Controller.csproj
         MediaBrowser.Controller/MediaBrowser.Controller.csproj
         MediaBrowser.Model/MediaBrowser.Model.csproj
         MediaBrowser.Model/MediaBrowser.Model.csproj
         Emby.Naming/Emby.Naming.csproj
         Emby.Naming/Emby.Naming.csproj
+        src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
       custom: 'pack'
       custom: 'pack'
       arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
       arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
 
 

+ 2 - 2
.ci/azure-pipelines-test.yml

@@ -10,7 +10,7 @@ parameters:
   default: "tests/**/*Tests.csproj"
   default: "tests/**/*Tests.csproj"
 - name: DotNetSdkVersion
 - name: DotNetSdkVersion
   type: string
   type: string
-  default: 5.0.103
+  default: 6.0.x
 
 
 jobs:
 jobs:
   - job: Test
   - job: Test
@@ -94,5 +94,5 @@ jobs:
         displayName: 'Publish OpenAPI Artifact'
         displayName: 'Publish OpenAPI Artifact'
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
         condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
         inputs:
         inputs:
-          targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
+          targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
           artifactName: 'OpenAPI Spec'
           artifactName: 'OpenAPI Spec'

+ 3 - 2
.ci/azure-pipelines.yml

@@ -5,8 +5,6 @@ variables:
   value: 'tests/**/*Tests.csproj'
   value: 'tests/**/*Tests.csproj'
 - name: RestoreBuildProjects
 - name: RestoreBuildProjects
   value: 'Jellyfin.Server/Jellyfin.Server.csproj'
   value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-- name: DotNetSdkVersion
-  value: 5.0.103
 
 
 pr:
 pr:
   autoCancel: true
   autoCancel: true
@@ -57,6 +55,9 @@ jobs:
         Common:
         Common:
           NugetPackageName: Jellyfin.Common
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
           AssemblyFileName: MediaBrowser.Common.dll
+        Extensions:
+          NugetPackageName: Jellyfin.Extensions
+          AssemblyFileName: Jellyfin.Extensions.dll
       LinuxImage: 'ubuntu-latest'
       LinuxImage: 'ubuntu-latest'
 
 
 - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
 - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:

+ 1 - 0
.copr

@@ -0,0 +1 @@
+fedora

+ 0 - 1
.copr/Makefile

@@ -1 +0,0 @@
-../fedora/Makefile

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

@@ -1,49 +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 72, Chrome 80, Safari 13]
- - Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- - Playback: [Direct Play, Remux, Direct Stream, Transcode] 
- - 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

+ 5 - 1
.github/stale.yml

@@ -17,9 +17,13 @@ staleLabel: stale
 # Comment to post when marking an issue as stale. Set to `false` to disable
 # Comment to post when marking an issue as stale. Set to `false` to disable
 markComment: >
 markComment: >
   This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
   This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
-  
+
   If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
   If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
 
 
   This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
   This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
 # Comment to post when closing a stale issue. Set to `false` to disable
 # Comment to post when closing a stale issue. Set to `false` to disable
 closeComment: false
 closeComment: false
+
+# Disable automatic closing of pull requests
+pulls:
+  daysUntilClose: false

+ 7 - 5
.github/workflows/automation.yml

@@ -11,6 +11,7 @@ jobs:
   label:
   label:
     name: Labeling
     name: Labeling
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    if: ${{ github.repository == 'jellyfin/jellyfin' }}
     steps:
     steps:
       - name: Apply label
       - name: Apply label
         uses: eps1lon/actions-label-merge-conflict@v2.0.1
         uses: eps1lon/actions-label-merge-conflict@v2.0.1
@@ -22,9 +23,10 @@ jobs:
   project:
   project:
     name: Project board
     name: Project board
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    if: ${{ github.repository == 'jellyfin/jellyfin' }}
     steps:
     steps:
       - name: Remove from 'Current Release' project
       - name: Remove from 'Current Release' project
-        uses: alex-page/github-project-automation-plus@v0.7.1
+        uses: alex-page/github-project-automation-plus@v0.8.1
         if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         continue-on-error: true
         continue-on-error: true
         with:
         with:
@@ -33,7 +35,7 @@ jobs:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
 
       - name: Add to 'Release Next' project
       - name: Add to 'Release Next' project
-        uses: alex-page/github-project-automation-plus@v0.7.1
+        uses: alex-page/github-project-automation-plus@v0.8.1
         if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
         if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
         continue-on-error: true
         continue-on-error: true
         with:
         with:
@@ -42,7 +44,7 @@ jobs:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
 
       - name: Add to 'Current Release' project
       - name: Add to 'Current Release' project
-        uses: alex-page/github-project-automation-plus@v0.7.1
+        uses: alex-page/github-project-automation-plus@v0.8.1
         if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         continue-on-error: true
         continue-on-error: true
         with:
         with:
@@ -56,7 +58,7 @@ jobs:
         run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
         run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
 
 
       - name: Move issue to needs triage
       - name: Move issue to needs triage
-        uses: alex-page/github-project-automation-plus@v0.7.1
+        uses: alex-page/github-project-automation-plus@v0.8.1
         if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
         if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
         continue-on-error: true
         continue-on-error: true
         with:
         with:
@@ -65,7 +67,7 @@ jobs:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
 
       - name: Add issue to triage project
       - name: Add issue to triage project
-        uses: alex-page/github-project-automation-plus@v0.7.1
+        uses: alex-page/github-project-automation-plus@v0.8.1
         if: github.event.issue.pull_request == '' && github.event.action == 'opened'
         if: github.event.issue.pull_request == '' && github.event.action == 'opened'
         continue-on-error: true
         continue-on-error: true
         with:
         with:

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

@@ -24,7 +24,8 @@ jobs:
     - name: Setup .NET Core
     - name: Setup .NET Core
       uses: actions/setup-dotnet@v1
       uses: actions/setup-dotnet@v1
       with:
       with:
-        dotnet-version: '5.0.x'
+        dotnet-version: '6.0.x'
+
     - name: Initialize CodeQL
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v1
       uses: github/codeql-action/init@v1
       with:
       with:

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

@@ -29,7 +29,7 @@ jobs:
           fetch-depth: 0
           fetch-depth: 0
 
 
       - name: Automatic Rebase
       - name: Automatic Rebase
-        uses: cirrus-actions/rebase@1.4
+        uses: cirrus-actions/rebase@1.5
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
 
 

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

+ 3 - 0
.gitignore

@@ -278,3 +278,6 @@ web/
 web-src.*
 web-src.*
 MediaBrowser.WebDashboard/jellyfin-web
 MediaBrowser.WebDashboard/jellyfin-web
 apiclient/generated
 apiclient/generated
+
+# Omnisharp crash logs
+mono_crash.*.json

+ 2 - 2
.vscode/launch.json

@@ -6,7 +6,7 @@
             "type": "coreclr",
             "type": "coreclr",
             "request": "launch",
             "request": "launch",
             "preLaunchTask": "build",
             "preLaunchTask": "build",
-            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
+            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
             "args": [],
             "args": [],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
             "cwd": "${workspaceFolder}/Jellyfin.Server",
             "console": "internalConsole",
             "console": "internalConsole",
@@ -22,7 +22,7 @@
             "type": "coreclr",
             "type": "coreclr",
             "request": "launch",
             "request": "launch",
             "preLaunchTask": "build",
             "preLaunchTask": "build",
-            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
+            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
             "args": ["--nowebclient"],
             "args": ["--nowebclient"],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
             "cwd": "${workspaceFolder}/Jellyfin.Server",
             "console": "internalConsole",
             "console": "internalConsole",

+ 5 - 0
CONTRIBUTORS.md

@@ -46,6 +46,7 @@
  - [fruhnow](https://github.com/fruhnow)
  - [fruhnow](https://github.com/fruhnow)
  - [geilername](https://github.com/geilername)
  - [geilername](https://github.com/geilername)
  - [gnattu](https://github.com/gnattu)
  - [gnattu](https://github.com/gnattu)
+ - [GodTamIt](https://github.com/GodTamIt)
  - [grafixeyehero](https://github.com/grafixeyehero)
  - [grafixeyehero](https://github.com/grafixeyehero)
  - [h1nk](https://github.com/h1nk)
  - [h1nk](https://github.com/h1nk)
  - [hawken93](https://github.com/hawken93)
  - [hawken93](https://github.com/hawken93)
@@ -148,6 +149,8 @@
  - [skyfrk](https://github.com/skyfrk)
  - [skyfrk](https://github.com/skyfrk)
  - [ianjazz246](https://github.com/ianjazz246)
  - [ianjazz246](https://github.com/ianjazz246)
  - [peterspenler](https://github.com/peterspenler)
  - [peterspenler](https://github.com/peterspenler)
+ - [MBR-0001](https://github.com/MBR-0001)
+ - [jonas-resch](https://github.com/jonas-resch)
 
 
 # Emby Contributors
 # Emby Contributors
 
 
@@ -212,3 +215,5 @@
  - [Tim Hobbs](https://github.com/timhobbs)
  - [Tim Hobbs](https://github.com/timhobbs)
  - [SvenVandenbrande](https://github.com/SvenVandenbrande)
  - [SvenVandenbrande](https://github.com/SvenVandenbrande)
  - [olsh](https://github.com/olsh)
  - [olsh](https://github.com/olsh)
+ - [lbenini](https://github.com/lbenini)
+ - [gnuyent](https://github.com/gnuyent)

+ 17 - 0
Directory.Build.props

@@ -0,0 +1,17 @@
+<Project>
+  <!-- Sets defaults for all projects in the repo -->
+
+  <PropertyGroup>
+    <Nullable>enable</Nullable>
+    <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+  </PropertyGroup>
+
+</Project>

+ 31 - 19
Dockerfile

@@ -1,4 +1,8 @@
-ARG DOTNET_VERSION=5.0
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
+# Requires binfm_misc registration
+# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
+ARG DOTNET_VERSION=6.0
 
 
 FROM node:lts-alpine as web-builder
 FROM node:lts-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 ARG JELLYFIN_WEB_VERSION=master
@@ -8,15 +12,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
  && mv dist /dist
 
 
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
-
-FROM debian:buster-slim
+FROM debian:stable-slim as app
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 ARG DEBIAN_FRONTEND="noninteractive"
@@ -25,19 +21,17 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
 
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
 # https://github.com/intel/compute-runtime/releases
 # https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=20.3.2
-ARG IGC_VERSION=1.0.5435
-ARG NEO_VERSION=20.46.18421
-ARG LEVEL_ZERO_VERSION=1.0.18421
+ARG GMMLIB_VERSION=21.2.1
+ARG IGC_VERSION=1.0.8517
+ARG NEO_VERSION=21.35.20826
+ARG LEVEL_ZERO_VERSION=1.2.20826
 
 
 # Install dependencies:
 # Install dependencies:
 # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
 # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
+# curl: healthcheck
 RUN apt-get update \
 RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
  && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
  && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
  && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
  && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
  && apt-get update \
  && apt-get update \
@@ -68,14 +62,32 @@ RUN apt-get update \
  && chmod 777 /cache /config /media \
  && chmod 777 /cache /config /media \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
 
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 ENV LC_ALL en_US.UTF-8
 ENV LC_ALL en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANGUAGE en_US:en
 ENV LANGUAGE en_US:en
 
 
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
 EXPOSE 8096
 EXPOSE 8096
 VOLUME /cache /config /media
 VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
     "--cachedir", "/cache", \
     "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
     "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+     CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1

+ 26 - 19
Dockerfile.arm

@@ -1,8 +1,8 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM ONLY
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
 
 
 
 
 FROM node:lts-alpine as web-builder
 FROM node:lts-alpine as web-builder
@@ -13,19 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
  && mv dist /dist
 
 
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
-
-
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:buster-slim
+FROM arm32v7/debian:stable-slim as app
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 ARG DEBIAN_FRONTEND="noninteractive"
@@ -35,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
 
 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
+
+# curl: setup & healthcheck
 RUN apt-get update \
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
  && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
  curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
  curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
@@ -53,7 +44,7 @@ RUN apt-get update \
  vainfo \
  vainfo \
  libva2 \
  libva2 \
  locales \
  locales \
- && apt-get remove curl gnupg -y \
+ && apt-get remove gnupg -y \
  && apt-get clean autoclean -y \
  && apt-get clean autoclean -y \
  && apt-get autoremove -y \
  && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && rm -rf /var/lib/apt/lists/* \
@@ -61,17 +52,33 @@ RUN apt-get update \
  && chmod 777 /cache /config /media \
  && chmod 777 /cache /config /media \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
 
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 ENV LC_ALL en_US.UTF-8
 ENV LC_ALL en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANGUAGE en_US:en
 ENV LANGUAGE en_US:en
 
 
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
 EXPOSE 8096
 EXPOSE 8096
 VOLUME /cache /config /media
 VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
     "--cachedir", "/cache", \
     "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
     "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+     CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1

+ 26 - 17
Dockerfile.arm64

@@ -1,8 +1,8 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM64 ONLY
 #####################################
 #####################################
 # Requires binfm_misc registration
 # Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
 
 
 
 
 FROM node:lts-alpine as web-builder
 FROM node:lts-alpine as web-builder
@@ -13,18 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
  && mv dist /dist
 
 
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
-
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:buster-slim
+FROM arm64v8/debian:stable-slim as app
 
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 ARG DEBIAN_FRONTEND="noninteractive"
@@ -34,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
 
 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
+
+# curl: healcheck
 RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
 RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
  ffmpeg \
  ffmpeg \
  libssl-dev \
  libssl-dev \
@@ -43,6 +35,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
  libomxil-bellagio0 \
  libomxil-bellagio0 \
  libomxil-bellagio-bin \
  libomxil-bellagio-bin \
  locales \
  locales \
+ curl \
  && apt-get clean autoclean -y \
  && apt-get clean autoclean -y \
  && apt-get autoremove -y \
  && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
  && rm -rf /var/lib/apt/lists/* \
@@ -50,17 +43,33 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
  && chmod 777 /cache /config /media \
  && chmod 777 /cache /config /media \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
 
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 ENV LC_ALL en_US.UTF-8
 ENV LC_ALL en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANG en_US.UTF-8
 ENV LANGUAGE en_US:en
 ENV LANGUAGE en_US:en
 
 
+FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
+WORKDIR /repo
+COPY . .
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
 EXPOSE 8096
 EXPOSE 8096
 VOLUME /cache /config /media
 VOLUME /cache /config /media
 ENTRYPOINT ["./jellyfin/jellyfin", \
 ENTRYPOINT ["./jellyfin/jellyfin", \
     "--datadir", "/config", \
     "--datadir", "/config", \
     "--cachedir", "/cache", \
     "--cachedir", "/cache", \
     "--ffmpeg", "/usr/bin/ffmpeg"]
     "--ffmpeg", "/usr/bin/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+     CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1

+ 3 - 2
DvdLib/DvdLib.csproj

@@ -10,10 +10,11 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <AnalysisMode>AllDisabledByDefault</AnalysisMode>
+    <Nullable>disable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
 </Project>
 </Project>

+ 2 - 1
DvdLib/Ifo/Dvd.cs

@@ -2,6 +2,7 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 
 
@@ -76,7 +77,7 @@ namespace DvdLib.Ifo
 
 
         private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
         private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
         {
         {
-            var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
+            var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
 
 
             var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
             var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
                 allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
                 allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));

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

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 namespace Emby.Dlna.Configuration
 namespace Emby.Dlna.Configuration
@@ -74,7 +72,7 @@ namespace Emby.Dlna.Configuration
         /// <summary>
         /// <summary>
         /// Gets or sets the default user account that the dlna server uses.
         /// Gets or sets the default user account that the dlna server uses.
         /// </summary>
         /// </summary>
-        public string DefaultUserId { get; set; }
+        public string? DefaultUserId { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether playTo device profiles should be created.
         /// Gets or sets a value indicating whether playTo device profiles should be created.

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

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -140,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
         /// </summary>
         /// </summary>
         /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
         /// <param name="profile">The <see cref="DeviceProfile"/>.</param>
         /// <returns>The <see cref="User"/>.</returns>
         /// <returns>The <see cref="User"/>.</returns>
-        private User GetUser(DeviceProfile profile)
+        private User? GetUser(DeviceProfile profile)
         {
         {
             if (!string.IsNullOrEmpty(profile.UserId))
             if (!string.IsNullOrEmpty(profile.UserId))
             {
             {

File diff suppressed because it is too large
+ 251 - 584
Emby.Dlna/ContentDirectory/ControlHandler.cs


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

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

+ 3 - 3
Emby.Dlna/ControlResponse.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -8,9 +6,11 @@ namespace Emby.Dlna
 {
 {
     public class ControlResponse
     public class ControlResponse
     {
     {
-        public ControlResponse()
+        public ControlResponse(string xml, bool isSuccessful)
         {
         {
             Headers = new Dictionary<string, string>();
             Headers = new Dictionary<string, string>();
+            Xml = xml;
+            IsSuccessful = isSuccessful;
         }
         }
 
 
         public IDictionary<string, string> Headers { get; }
         public IDictionary<string, string> Headers { get; }

+ 15 - 17
Emby.Dlna/Didl/DidlBuilder.cs

@@ -41,8 +41,6 @@ namespace Emby.Dlna.Didl
         private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
         private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
         private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
         private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
 
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
         private readonly DeviceProfile _profile;
         private readonly DeviceProfile _profile;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
         private readonly string _serverAddress;
         private readonly string _serverAddress;
@@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl
 
 
             if (mediaSource.RunTimeTicks.HasValue)
             if (mediaSource.RunTimeTicks.HasValue)
             {
             {
-                writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+                writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
             }
             }
 
 
             if (filter.Contains("res@size"))
             if (filter.Contains("res@size"))
@@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl
 
 
                     if (size.HasValue)
                     if (size.HasValue)
                     {
                     {
-                        writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+                        writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
                     }
                     }
                 }
                 }
             }
             }
@@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl
 
 
             if (targetChannels.HasValue)
             if (targetChannels.HasValue)
             {
             {
-                writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+                writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
             }
             }
 
 
             if (filter.Contains("res@resolution"))
             if (filter.Contains("res@resolution"))
@@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl
 
 
             if (targetSampleRate.HasValue)
             if (targetSampleRate.HasValue)
             {
             {
-                writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+                writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
             }
             }
 
 
             if (totalBitrate.HasValue)
             if (totalBitrate.HasValue)
             {
             {
-                writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
+                writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
             }
             }
 
 
             var mediaProfile = _profile.GetVideoMediaProfile(
             var mediaProfile = _profile.GetVideoMediaProfile(
@@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl
 
 
             if (mediaSource.RunTimeTicks.HasValue)
             if (mediaSource.RunTimeTicks.HasValue)
             {
             {
-                writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+                writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
             }
             }
 
 
             if (filter.Contains("res@size"))
             if (filter.Contains("res@size"))
@@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl
 
 
                     if (size.HasValue)
                     if (size.HasValue)
                     {
                     {
-                        writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+                        writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
                     }
                     }
                 }
                 }
             }
             }
@@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl
 
 
             if (targetChannels.HasValue)
             if (targetChannels.HasValue)
             {
             {
-                writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+                writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
             }
             }
 
 
             if (targetSampleRate.HasValue)
             if (targetSampleRate.HasValue)
             {
             {
-                writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+                writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
             }
             }
 
 
             if (targetAudioBitrate.HasValue)
             if (targetAudioBitrate.HasValue)
             {
             {
-                writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
+                writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
             }
             }
 
 
             var mediaProfile = _profile.GetAudioMediaProfile(
             var mediaProfile = _profile.GetAudioMediaProfile(
@@ -639,7 +637,7 @@ namespace Emby.Dlna.Didl
 
 
             writer.WriteAttributeString("restricted", "1");
             writer.WriteAttributeString("restricted", "1");
             writer.WriteAttributeString("searchable", "1");
             writer.WriteAttributeString("searchable", "1");
-            writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
+            writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
 
 
             var clientId = GetClientId(folder, stubType);
             var clientId = GetClientId(folder, stubType);
 
 
@@ -731,7 +729,7 @@ namespace Emby.Dlna.Didl
             {
             {
                 if (item.PremiereDate.HasValue)
                 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);
                 }
                 }
             }
             }
 
 
@@ -748,7 +746,7 @@ namespace Emby.Dlna.Didl
                 AddValue(writer, "upnp", "publisher", studio, NsUpnp);
                 AddValue(writer, "upnp", "publisher", studio, NsUpnp);
             }
             }
 
 
-            if (!(item is Folder))
+            if (item is not Folder)
             {
             {
                 if (filter.Contains("dc:description"))
                 if (filter.Contains("dc:description"))
                 {
                 {
@@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl
 
 
             if (item.IndexNumber.HasValue)
             if (item.IndexNumber.HasValue)
             {
             {
-                AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+                AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
 
 
                 if (item is Episode)
                 if (item is Episode)
                 {
                 {
-                    AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+                    AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
                 }
                 }
             }
             }
         }
         }

+ 56 - 57
Emby.Dlna/DlnaManager.cs

@@ -1,22 +1,18 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
-
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
-using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna.Profiles;
 using Emby.Dlna.Profiles;
 using Emby.Dlna.Server;
 using Emby.Dlna.Server;
+using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
@@ -96,12 +92,14 @@ namespace Emby.Dlna
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public DeviceProfile GetDefaultProfile()
         public DeviceProfile GetDefaultProfile()
         {
         {
             return new DefaultProfile();
             return new DefaultProfile();
         }
         }
 
 
-        public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
         {
         {
             if (deviceInfo == null)
             if (deviceInfo == null)
             {
             {
@@ -111,35 +109,18 @@ namespace Emby.Dlna
             var profile = GetProfiles()
             var profile = GetProfiles()
                 .FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
                 .FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
 
 
-            if (profile != null)
+            if (profile == null)
             {
             {
-                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
+                _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
             }
             }
             else
             else
             {
             {
-                LogUnmatchedProfile(deviceInfo);
+                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
             }
             }
 
 
             return profile;
             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>
         /// <summary>
         /// Attempts to match a device with a profile.
         /// Attempts to match a device with a profile.
         /// Rules:
         /// Rules:
@@ -187,7 +168,8 @@ namespace Emby.Dlna
             }
             }
         }
         }
 
 
-        public DeviceProfile GetProfile(IHeaderDictionary headers)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(IHeaderDictionary headers)
         {
         {
             if (headers == null)
             if (headers == null)
             {
             {
@@ -195,15 +177,13 @@ namespace Emby.Dlna
             }
             }
 
 
             var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
             var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
-
-            if (profile != null)
+            if (profile == null)
             {
             {
-                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+                _logger.LogDebug("No matching device profile found. {@Headers}", headers);
             }
             }
             else
             else
             {
             {
-                var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
-                _logger.LogDebug("No matching device profile found. {0}", headerString);
+                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
             }
             }
 
 
             return profile;
             return profile;
@@ -253,19 +233,19 @@ namespace Emby.Dlna
                 return xmlFies
                 return xmlFies
                     .Select(i => ParseProfileFile(i, type))
                     .Select(i => ParseProfileFile(i, type))
                     .Where(i => i != null)
                     .Where(i => i != null)
-                    .ToList();
+                    .ToList()!; // We just filtered out all the nulls
             }
             }
             catch (IOException)
             catch (IOException)
             {
             {
-                return new List<DeviceProfile>();
+                return Array.Empty<DeviceProfile>();
             }
             }
         }
         }
 
 
-        private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
+        private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
         {
         {
             lock (_profiles)
             lock (_profiles)
             {
             {
-                if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
+                if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
                 {
                 {
                     return profileTuple.Item2;
                     return profileTuple.Item2;
                 }
                 }
@@ -293,7 +273,8 @@ namespace Emby.Dlna
             }
             }
         }
         }
 
 
-        public DeviceProfile GetProfile(string id)
+        /// <inheritdoc />
+        public DeviceProfile? GetProfile(string id)
         {
         {
             if (string.IsNullOrEmpty(id))
             if (string.IsNullOrEmpty(id))
             {
             {
@@ -322,6 +303,7 @@ namespace Emby.Dlna
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public IEnumerable<DeviceProfileInfo> GetProfileInfos()
         public IEnumerable<DeviceProfileInfo> GetProfileInfos()
         {
         {
             return GetProfileInfosInternal().Select(i => i.Info);
             return GetProfileInfosInternal().Select(i => i.Info);
@@ -329,17 +311,14 @@ namespace Emby.Dlna
 
 
         private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
         private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
         {
         {
-            return new InternalProfileInfo
-            {
-                Path = file.FullName,
-
-                Info = new DeviceProfileInfo
+            return new InternalProfileInfo(
+                new DeviceProfileInfo
                 {
                 {
                     Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
                     Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
                     Name = _fileSystem.GetFileNameWithoutExtension(file),
                     Name = _fileSystem.GetFileNameWithoutExtension(file),
                     Type = type
                     Type = type
-                }
-            };
+                },
+                file.FullName);
         }
         }
 
 
         private async Task ExtractSystemProfilesAsync()
         private async Task ExtractSystemProfilesAsync()
@@ -359,16 +338,20 @@ namespace Emby.Dlna
                     systemProfilesPath,
                     systemProfilesPath,
                     Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
                     Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
 
 
-                using (var stream = _assembly.GetManifestResourceStream(name))
+                // The stream should exist as we just got its name from GetManifestResourceNames
+                using (var stream = _assembly.GetManifestResourceStream(name)!)
                 {
                 {
+                    var length = stream.Length;
                     var fileInfo = _fileSystem.GetFileInfo(path);
                     var fileInfo = _fileSystem.GetFileInfo(path);
 
 
-                    if (!fileInfo.Exists || fileInfo.Length != stream.Length)
+                    if (!fileInfo.Exists || fileInfo.Length != length)
                     {
                     {
                         Directory.CreateDirectory(systemProfilesPath);
                         Directory.CreateDirectory(systemProfilesPath);
 
 
-                        // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
-                        using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+                        var fileOptions = AsyncFile.WriteOptions;
+                        fileOptions.Mode = FileMode.Create;
+                        fileOptions.PreallocationSize = length;
+                        using (var fileStream = new FileStream(path, fileOptions))
                         {
                         {
                             await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                             await stream.CopyToAsync(fileStream).ConfigureAwait(false);
                         }
                         }
@@ -380,6 +363,7 @@ namespace Emby.Dlna
             Directory.CreateDirectory(UserProfilesPath);
             Directory.CreateDirectory(UserProfilesPath);
         }
         }
 
 
+        /// <inheritdoc />
         public void DeleteProfile(string id)
         public void DeleteProfile(string id)
         {
         {
             var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
             var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@@ -397,6 +381,7 @@ namespace Emby.Dlna
             }
             }
         }
         }
 
 
+        /// <inheritdoc />
         public void CreateProfile(DeviceProfile profile)
         public void CreateProfile(DeviceProfile profile)
         {
         {
             profile = ReserializeProfile(profile);
             profile = ReserializeProfile(profile);
@@ -412,7 +397,8 @@ namespace Emby.Dlna
             SaveProfile(profile, path, DeviceProfileType.User);
             SaveProfile(profile, path, DeviceProfileType.User);
         }
         }
 
 
-        public void UpdateProfile(DeviceProfile profile)
+        /// <inheritdoc />
+        public void UpdateProfile(string profileId, DeviceProfile profile)
         {
         {
             profile = ReserializeProfile(profile);
             profile = ReserializeProfile(profile);
 
 
@@ -426,7 +412,7 @@ namespace Emby.Dlna
                 throw new ArgumentException("Profile is missing Name");
                 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 newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
             var path = Path.Combine(UserProfilesPath, newFilename);
             var path = Path.Combine(UserProfilesPath, newFilename);
@@ -470,9 +456,11 @@ namespace Emby.Dlna
 
 
             var json = JsonSerializer.Serialize(profile, _jsonOptions);
             var json = JsonSerializer.Serialize(profile, _jsonOptions);
 
 
-            return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
+            // Output can't be null if the input isn't null
+            return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
         }
         }
 
 
+        /// <inheritdoc />
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
         {
         {
             var profile = GetDefaultProfile();
             var profile = GetDefaultProfile();
@@ -482,26 +470,37 @@ namespace Emby.Dlna
             return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
             return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
         }
         }
 
 
-        public ImageStream GetIcon(string filename)
+        /// <inheritdoc />
+        public ImageStream? GetIcon(string filename)
         {
         {
             var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
             var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
                 ? ImageFormat.Png
                 ? ImageFormat.Png
                 : ImageFormat.Jpg;
                 : ImageFormat.Jpg;
 
 
             var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
             var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
+            var stream = _assembly.GetManifestResourceStream(resource);
+            if (stream == null)
+            {
+                return null;
+            }
 
 
-            return new ImageStream
+            return new ImageStream(stream)
             {
             {
-                Format = format,
-                Stream = _assembly.GetManifestResourceStream(resource)
+                Format = format
             };
             };
         }
         }
 
 
         private class InternalProfileInfo
         private class InternalProfileInfo
         {
         {
-            internal DeviceProfileInfo Info { get; set; }
+            internal InternalProfileInfo(DeviceProfileInfo info, string path)
+            {
+                Info = info;
+                Path = path;
+            }
+
+            internal DeviceProfileInfo Info { get; }
 
 
-            internal string Path { get; set; }
+            internal string Path { get; }
         }
         }
     }
     }
 
 

+ 2 - 8
Emby.Dlna/Emby.Dlna.csproj

@@ -17,11 +17,9 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-    <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
@@ -31,10 +29,6 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Images\logo120.jpg" />
     <EmbeddedResource Include="Images\logo120.jpg" />
     <EmbeddedResource Include="Images\logo120.png" />
     <EmbeddedResource Include="Images\logo120.png" />
@@ -78,7 +72,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

+ 3 - 3
Emby.Dlna/EventSubscriptionResponse.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -8,8 +6,10 @@ namespace Emby.Dlna
 {
 {
     public class EventSubscriptionResponse
     public class EventSubscriptionResponse
     {
     {
-        public EventSubscriptionResponse()
+        public EventSubscriptionResponse(string content, string contentType)
         {
         {
+            Content = content;
+            ContentType = contentType;
             Headers = new Dictionary<string, string>();
             Headers = new Dictionary<string, string>();
         }
         }
 
 

+ 7 - 22
Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -11,6 +11,7 @@ using System.Net.Http;
 using System.Net.Mime;
 using System.Net.Mime;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -25,8 +26,6 @@ namespace Emby.Dlna.Eventing
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
 
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
         public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
         public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
         {
         {
             _httpClientFactory = httpClientFactory;
             _httpClientFactory = httpClientFactory;
@@ -51,11 +50,7 @@ namespace Emby.Dlna.Eventing
                 return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
                 return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
             }
             }
 
 
-            return new EventSubscriptionResponse
-            {
-                Content = string.Empty,
-                ContentType = "text/plain"
-            };
+            return new EventSubscriptionResponse(string.Empty, "text/plain");
         }
         }
 
 
         public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
         public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@@ -86,9 +81,7 @@ namespace Emby.Dlna.Eventing
             if (!string.IsNullOrEmpty(header))
             if (!string.IsNullOrEmpty(header))
             {
             {
                 // Starts with SECOND-
                 // Starts with SECOND-
-                header = header.Split('-')[^1];
-
-                if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
+                if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                 {
                 {
                     return val;
                     return val;
                 }
                 }
@@ -103,23 +96,15 @@ namespace Emby.Dlna.Eventing
 
 
             _subscriptions.TryRemove(subscriptionId, out _);
             _subscriptions.TryRemove(subscriptionId, out _);
 
 
-            return new EventSubscriptionResponse
-            {
-                Content = string.Empty,
-                ContentType = "text/plain"
-            };
+            return new EventSubscriptionResponse(string.Empty, "text/plain");
         }
         }
 
 
         private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
         private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
         {
         {
-            var response = new EventSubscriptionResponse
-            {
-                Content = string.Empty,
-                ContentType = "text/plain"
-            };
+            var response = new EventSubscriptionResponse(string.Empty, "text/plain");
 
 
             response.Headers["SID"] = subscriptionId;
             response.Headers["SID"] = subscriptionId;
-            response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
+            response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
 
 
             return response;
             return response;
         }
         }
@@ -176,7 +161,7 @@ namespace Emby.Dlna.Eventing
             options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
             options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
             options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
             options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
             options.Headers.TryAddWithoutValidation("SID", subscription.Id);
             options.Headers.TryAddWithoutValidation("SID", subscription.Id);
-            options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
+            options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
 
 
             try
             try
             {
             {

+ 12 - 23
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -27,11 +27,9 @@ using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Rssdp;
 using Rssdp;
 using Rssdp.Infrastructure;
 using Rssdp.Infrastructure;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 
 namespace Emby.Dlna.Main
 namespace Emby.Dlna.Main
 {
 {
@@ -54,7 +52,6 @@ namespace Emby.Dlna.Main
         private readonly ISocketFactory _socketFactory;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
         private readonly object _syncLock = new object();
         private readonly object _syncLock = new object();
-        private readonly NetworkConfiguration _netConfig;
         private readonly bool _disabled;
         private readonly bool _disabled;
 
 
         private PlayToManager _manager;
         private PlayToManager _manager;
@@ -127,8 +124,8 @@ namespace Emby.Dlna.Main
                 config);
                 config);
             Current = this;
             Current = this;
 
 
-            _netConfig = config.GetConfiguration<NetworkConfiguration>("network");
-            _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
+            var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
+            _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
 
 
             if (_disabled && _config.GetDlnaConfiguration().EnableServer)
             if (_disabled && _config.GetDlnaConfiguration().EnableServer)
             {
             {
@@ -204,8 +201,8 @@ namespace Emby.Dlna.Main
             {
             {
                 if (_communicationsServer == null)
                 if (_communicationsServer == null)
                 {
                 {
-                    var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
-                                                   OperatingSystem.Id == OperatingSystemId.Linux;
+                    var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
+                                                   OperatingSystem.IsLinux();
 
 
                     _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
                     _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
                     {
                     {
@@ -221,11 +218,6 @@ namespace Emby.Dlna.Main
             }
             }
         }
         }
 
 
-        private void LogMessage(string msg)
-        {
-            _logger.LogDebug(msg);
-        }
-
         private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
         private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
         {
         {
             try
             try
@@ -268,9 +260,13 @@ namespace Emby.Dlna.Main
 
 
             try
             try
             {
             {
-                _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+                _publisher = new SsdpDevicePublisher(
+                    _communicationsServer,
+                    MediaBrowser.Common.System.OperatingSystem.Name,
+                    Environment.OSVersion.VersionString,
+                    _config.GetDlnaConfiguration().SendOnlyMatchedHost)
                 {
                 {
-                    LogFunction = LogMessage,
+                    LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
                     SupportPnpRootDevice = false
                     SupportPnpRootDevice = false
                 };
                 };
 
 
@@ -315,15 +311,9 @@ namespace Emby.Dlna.Main
 
 
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
                 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
                 var device = new SsdpRootDevice
                 {
                 {
@@ -409,7 +399,6 @@ namespace Emby.Dlna.Main
                         _imageProcessor,
                         _imageProcessor,
                         _deviceDiscovery,
                         _deviceDiscovery,
                         _httpClientFactory,
                         _httpClientFactory,
-                        _config,
                         _userDataManager,
                         _userDataManager,
                         _localization,
                         _localization,
                         _mediaSourceManager,
                         _mediaSourceManager,

+ 9 - 24
Emby.Dlna/PlayTo/Device.cs

@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class Device : IDisposable
     public class Device : IDisposable
     {
     {
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
 
 
         private readonly ILogger _logger;
         private readonly ILogger _logger;
@@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo
                 return;
                 return;
             }
             }
 
 
-            Volume = int.Parse(volumeValue, UsCulture);
+            Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
 
 
             if (Volume > 0)
             if (Volume > 0)
             {
             {
@@ -842,7 +840,7 @@ namespace Emby.Dlna.PlayTo
             if (!string.IsNullOrWhiteSpace(duration)
             if (!string.IsNullOrWhiteSpace(duration)
                 && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
                 && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
             {
-                Duration = TimeSpan.Parse(duration, UsCulture);
+                Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
             }
             }
             else
             else
             {
             {
@@ -854,7 +852,7 @@ namespace Emby.Dlna.PlayTo
 
 
             if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
             {
             {
-                Position = TimeSpan.Parse(position, UsCulture);
+                Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
             }
             }
 
 
             var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
             var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@@ -1194,8 +1192,8 @@ namespace Emby.Dlna.PlayTo
             var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
             var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
             var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
             var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
 
 
-            var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
-            var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
+            var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
+            var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
 
 
             return new DeviceIcon
             return new DeviceIcon
             {
             {
@@ -1260,10 +1258,7 @@ namespace Emby.Dlna.PlayTo
                 return;
                 return;
             }
             }
 
 
-            PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
-            {
-                MediaInfo = mediaInfo
-            });
+            PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
         }
         }
 
 
         private void OnPlaybackProgress(UBaseObject mediaInfo)
         private void OnPlaybackProgress(UBaseObject mediaInfo)
@@ -1273,27 +1268,17 @@ namespace Emby.Dlna.PlayTo
                 return;
                 return;
             }
             }
 
 
-            PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
-            {
-                MediaInfo = mediaInfo
-            });
+            PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
         }
         }
 
 
         private void OnPlaybackStop(UBaseObject mediaInfo)
         private void OnPlaybackStop(UBaseObject mediaInfo)
         {
         {
-            PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
-            {
-                MediaInfo = mediaInfo
-            });
+            PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
         }
         }
 
 
         private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
         private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
         {
         {
-            MediaChanged?.Invoke(this, new MediaChangedEventArgs
-            {
-                OldMediaInfo = old,
-                NewMediaInfo = newMedia
-            });
+            MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />

+ 7 - 3
Emby.Dlna/PlayTo/MediaChangedEventArgs.cs

@@ -1,6 +1,4 @@
-#nullable disable
-
-#pragma warning disable CS1591
+#pragma warning disable CS1591
 
 
 using System;
 using System;
 
 
@@ -8,6 +6,12 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class MediaChangedEventArgs : EventArgs
     public class MediaChangedEventArgs : EventArgs
     {
     {
+        public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
+        {
+            OldMediaInfo = oldMediaInfo;
+            NewMediaInfo = newMediaInfo;
+        }
+
         public UBaseObject OldMediaInfo { get; set; }
         public UBaseObject OldMediaInfo { get; set; }
 
 
         public UBaseObject NewMediaInfo { get; set; }
         public UBaseObject NewMediaInfo { get; set; }

+ 3 - 5
Emby.Dlna/PlayTo/PlayToController.cs

@@ -30,8 +30,6 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class PlayToController : ISessionController, IDisposable
     public class PlayToController : ISessionController, IDisposable
     {
     {
-        private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
         private readonly SessionInfo _session;
         private readonly SessionInfo _session;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
@@ -716,7 +714,7 @@ namespace Emby.Dlna.PlayTo
                 case GeneralCommandType.SetAudioStreamIndex:
                 case GeneralCommandType.SetAudioStreamIndex:
                     if (command.Arguments.TryGetValue("Index", out string index))
                     if (command.Arguments.TryGetValue("Index", out string index))
                     {
                     {
-                        if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+                        if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                         {
                         {
                             return SetAudioStreamIndex(val);
                             return SetAudioStreamIndex(val);
                         }
                         }
@@ -728,7 +726,7 @@ namespace Emby.Dlna.PlayTo
                 case GeneralCommandType.SetSubtitleStreamIndex:
                 case GeneralCommandType.SetSubtitleStreamIndex:
                     if (command.Arguments.TryGetValue("Index", out index))
                     if (command.Arguments.TryGetValue("Index", out index))
                     {
                     {
-                        if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+                        if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                         {
                         {
                             return SetSubtitleStreamIndex(val);
                             return SetSubtitleStreamIndex(val);
                         }
                         }
@@ -740,7 +738,7 @@ namespace Emby.Dlna.PlayTo
                 case GeneralCommandType.SetVolume:
                 case GeneralCommandType.SetVolume:
                     if (command.Arguments.TryGetValue("Volume", out string vol))
                     if (command.Arguments.TryGetValue("Volume", out string vol))
                     {
                     {
-                        if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+                        if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
                         {
                         {
                             return _device.SetVolume(volume, cancellationToken);
                             return _device.SetVolume(volume, cancellationToken);
                         }
                         }

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

@@ -11,7 +11,6 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly IServerConfigurationManager _config;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
         private readonly ILocalizationManager _localization;
 
 
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
         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;
             _logger = logger;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
             _deviceDiscovery = deviceDiscovery;
             _deviceDiscovery = deviceDiscovery;
             _httpClientFactory = httpClientFactory;
             _httpClientFactory = httpClientFactory;
-            _config = config;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _localization = localization;
             _localization = localization;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
@@ -173,7 +170,9 @@ namespace Emby.Dlna.PlayTo
                 uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
                 uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
             }
             }
 
 
-            var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
+            var sessionInfo = await _sessionManager
+                .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
+                .ConfigureAwait(false);
 
 
             var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
             var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
 
 

+ 5 - 2
Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class PlaybackProgressEventArgs : EventArgs
     public class PlaybackProgressEventArgs : EventArgs
     {
     {
+        public PlaybackProgressEventArgs(UBaseObject mediaInfo)
+        {
+            MediaInfo = mediaInfo;
+        }
+
         public UBaseObject MediaInfo { get; set; }
         public UBaseObject MediaInfo { get; set; }
     }
     }
 }
 }

+ 5 - 2
Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class PlaybackStartEventArgs : EventArgs
     public class PlaybackStartEventArgs : EventArgs
     {
     {
+        public PlaybackStartEventArgs(UBaseObject mediaInfo)
+        {
+            MediaInfo = mediaInfo;
+        }
+
         public UBaseObject MediaInfo { get; set; }
         public UBaseObject MediaInfo { get; set; }
     }
     }
 }
 }

+ 5 - 2
Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
 {
 {
     public class PlaybackStoppedEventArgs : EventArgs
     public class PlaybackStoppedEventArgs : EventArgs
     {
     {
+        public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
+        {
+            MediaInfo = mediaInfo;
+        }
+
         public UBaseObject MediaInfo { get; set; }
         public UBaseObject MediaInfo { get; set; }
     }
     }
 }
 }

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

@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
         private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
         private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
         private const string FriendlyName = "Jellyfin";
         private const string FriendlyName = "Jellyfin";
 
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
 
 
         public SsdpHttpClient(IHttpClientFactory httpClientFactory)
         public SsdpHttpClient(IHttpClientFactory httpClientFactory)
@@ -45,10 +43,12 @@ namespace Emby.Dlna.PlayTo
                     header,
                     header,
                     cancellationToken)
                     cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
+
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             return await XDocument.LoadAsync(
             return await XDocument.LoadAsync(
                 stream,
                 stream,
-                LoadOptions.PreserveWhitespace,
+                LoadOptions.None,
                 cancellationToken).ConfigureAwait(false);
                 cancellationToken).ConfigureAwait(false);
         }
         }
 
 
@@ -78,14 +78,15 @@ namespace Emby.Dlna.PlayTo
         {
         {
             using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
             using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
             options.Headers.UserAgent.ParseAdd(USERAGENT);
             options.Headers.UserAgent.ParseAdd(USERAGENT);
-            options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
-            options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+            options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
+            options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
             options.Headers.TryAddWithoutValidation("NT", "upnp:event");
             options.Headers.TryAddWithoutValidation("NT", "upnp:event");
-            options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+            options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
 
 
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
                 .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
                 .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
         }
         }
 
 
         public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
         public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
@@ -94,12 +95,13 @@ namespace Emby.Dlna.PlayTo
             options.Headers.UserAgent.ParseAdd(USERAGENT);
             options.Headers.UserAgent.ParseAdd(USERAGENT);
             options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
             options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             try
             try
             {
             {
                 return await XDocument.LoadAsync(
                 return await XDocument.LoadAsync(
                     stream,
                     stream,
-                    LoadOptions.PreserveWhitespace,
+                    LoadOptions.None,
                     cancellationToken).ConfigureAwait(false);
                     cancellationToken).ConfigureAwait(false);
             }
             }
             catch
             catch

+ 3 - 5
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -15,7 +15,6 @@ namespace Emby.Dlna.Server
     {
     {
         private readonly DeviceProfile _profile;
         private readonly DeviceProfile _profile;
 
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly string _serverUdn;
         private readonly string _serverUdn;
         private readonly string _serverAddress;
         private readonly string _serverAddress;
         private readonly string _serverName;
         private readonly string _serverName;
@@ -193,10 +192,10 @@ namespace Emby.Dlna.Server
                     .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
                     .Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
                     .Append("</mimetype>");
                     .Append("</mimetype>");
                 builder.Append("<width>")
                 builder.Append("<width>")
-                    .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+                    .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
                     .Append("</width>");
                     .Append("</width>");
                 builder.Append("<height>")
                 builder.Append("<height>")
-                    .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+                    .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
                     .Append("</height>");
                     .Append("</height>");
                 builder.Append("<depth>")
                 builder.Append("<depth>")
                     .Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
                     .Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
@@ -250,8 +249,7 @@ namespace Emby.Dlna.Server
 
 
             url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
             url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
 
 
-            // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
-            return SecurityElement.Escape(url) ?? string.Empty;
+            return SecurityElement.Escape(url);
         }
         }
 
 
         private IEnumerable<DeviceIcon> GetIcons()
         private IEnumerable<DeviceIcon> GetIcons()

+ 3 - 7
Emby.Dlna/Service/BaseControlHandler.cs

@@ -6,9 +6,9 @@ using System.IO;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
+using Diacritics.Extensions;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Didl;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Dlna.Service
 namespace Emby.Dlna.Service
@@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
                 requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
                 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
             var settings = new XmlWriterSettings
             {
             {
@@ -95,11 +95,7 @@ namespace Emby.Dlna.Service
 
 
             var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
             var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
 
 
-            var controlResponse = new ControlResponse
-            {
-                Xml = xml,
-                IsSuccessful = true
-            };
+            var controlResponse = new ControlResponse(xml, true);
 
 
             controlResponse.Headers.Add("EXT", string.Empty);
             controlResponse.Headers.Add("EXT", string.Empty);
 
 

+ 4 - 4
Emby.Dlna/Service/BaseService.cs

@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
             return EventManager.CancelEventSubscription(subscriptionId);
             return EventManager.CancelEventSubscription(subscriptionId);
         }
         }
 
 
-        public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
+        public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
         {
         {
-            return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
+            return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
         }
         }
 
 
-        public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
+        public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
         {
         {
-            return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
+            return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
         }
         }
     }
     }
 }
 }

+ 1 - 5
Emby.Dlna/Service/ControlErrorHandler.cs

@@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
                 writer.WriteEndDocument();
                 writer.WriteEndDocument();
             }
             }
 
 
-            return new ControlResponse
-            {
-                Xml = builder.ToString(),
-                IsSuccessful = false
-            };
+            return new ControlResponse(builder.ToString(), false);
         }
         }
     }
     }
 }
 }

+ 1 - 7
Emby.Drawing/Emby.Drawing.csproj

@@ -6,11 +6,9 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-    <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -30,8 +28,4 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
 </Project>
 </Project>

+ 2 - 2
Emby.Drawing/ImageProcessor.cs

@@ -26,7 +26,7 @@ namespace Emby.Drawing
     public sealed class ImageProcessor : IImageProcessor, IDisposable
     public sealed class ImageProcessor : IImageProcessor, IDisposable
     {
     {
         // Increment this when there's a change requiring caches to be invalidated
         // 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
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
@@ -102,7 +102,7 @@ namespace Emby.Drawing
         {
         {
             var file = await ProcessImage(options).ConfigureAwait(false);
             var file = await ProcessImage(options).ConfigureAwait(false);
 
 
-            using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+            using (var fileStream = AsyncFile.OpenRead(file.Item1))
             {
             {
                 await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
                 await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
             }
             }

+ 1 - 1
Emby.Naming/Audio/AudioFileParser.cs

@@ -1,7 +1,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.Audio
 namespace Emby.Naming.Audio
 {
 {

+ 4 - 4
Emby.Naming/AudioBook/AudioBookInfo.cs

@@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
         /// <param name="files">List of files composing the actual audiobook.</param>
         /// <param name="files">List of files composing the actual audiobook.</param>
         /// <param name="extras">List of extra files.</param>
         /// <param name="extras">List of extra files.</param>
         /// <param name="alternateVersions">Alternative version of files.</param>
         /// <param name="alternateVersions">Alternative version of files.</param>
-        public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
+        public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
         {
         {
             Name = name;
             Name = name;
             Year = year;
             Year = year;
@@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
         /// Gets or sets the files.
         /// Gets or sets the files.
         /// </summary>
         /// </summary>
         /// <value>The files.</value>
         /// <value>The files.</value>
-        public List<AudioBookFileInfo> Files { get; set; }
+        public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the extras.
         /// Gets or sets the extras.
         /// </summary>
         /// </summary>
         /// <value>The extras.</value>
         /// <value>The extras.</value>
-        public List<AudioBookFileInfo> Extras { get; set; }
+        public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the alternate versions.
         /// Gets or sets the alternate versions.
         /// </summary>
         /// </summary>
         /// <value>The alternate versions.</value>
         /// <value>The alternate versions.</value>
-        public List<AudioBookFileInfo> AlternateVersions { get; set; }
+        public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
     }
     }
 }
 }

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

@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
     public class AudioBookListResolver
     public class AudioBookListResolver
     {
     {
         private readonly NamingOptions _options;
         private readonly NamingOptions _options;
+        private readonly AudioBookResolver _audioBookResolver;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
         /// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
         public AudioBookListResolver(NamingOptions options)
         public AudioBookListResolver(NamingOptions options)
         {
         {
             _options = options;
             _options = options;
+            _audioBookResolver = new AudioBookResolver(_options);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook
         /// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
         /// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
         public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
         public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
         {
         {
-            var audioBookResolver = new AudioBookResolver(_options);
 
 
             // File with empty fullname will be sorted out here.
             // File with empty fullname will be sorted out here.
             var audiobookFileInfos = files
             var audiobookFileInfos = files
-                .Select(i => audioBookResolver.Resolve(i.FullName))
+                .Select(i => _audioBookResolver.Resolve(i.FullName))
                 .OfType<AudioBookFileInfo>()
                 .OfType<AudioBookFileInfo>()
                 .ToList();
                 .ToList();
 
 
-            var stackResult = new StackResolver(_options)
-                .ResolveAudioBooks(audiobookFileInfos);
+            var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
 
 
             foreach (var stack in stackResult)
             foreach (var stack in stackResult)
             {
             {
                 var stackFiles = stack.Files
                 var stackFiles = stack.Files
-                    .Select(i => audioBookResolver.Resolve(i))
+                    .Select(i => _audioBookResolver.Resolve(i))
                     .OfType<AudioBookFileInfo>()
                     .OfType<AudioBookFileInfo>()
                     .ToList();
                     .ToList();
 
 
@@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
                         foreach (var audioFile in group)
                         foreach (var audioFile in group)
                         {
                         {
                             var name = Path.GetFileNameWithoutExtension(audioFile.Path);
                             var name = Path.GetFileNameWithoutExtension(audioFile.Path);
-                            if (name.Equals("audiobook") ||
+                            if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
                                 name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
                                 name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
                                 name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
                                 name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
                             {
                             {

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

@@ -1,7 +1,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.AudioBook
 namespace Emby.Naming.AudioBook
 {
 {
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
             var extension = Path.GetExtension(path);
             var extension = Path.GetExtension(path);
 
 
             // Check supported extensions
             // Check supported extensions
-            if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return null;
                 return null;
             }
             }

+ 75 - 21
Emby.Naming/Common/NamingOptions.cs

@@ -1,4 +1,7 @@
+#pragma warning disable CA1819
+
 using System;
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using Emby.Naming.Video;
 using Emby.Naming.Video;
@@ -122,11 +125,11 @@ namespace Emby.Naming.Common
                     token: "DSR")
                     token: "DSR")
             };
             };
 
 
-            VideoFileStackingExpressions = new[]
+            VideoFileStackingRules = new[]
             {
             {
-                "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
-                "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
-                "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
             };
             };
 
 
             CleanDateTimes = new[]
             CleanDateTimes = new[]
@@ -137,8 +140,11 @@ namespace Emby.Naming.Common
 
 
             CleanStrings = new[]
             CleanStrings = new[]
             {
             {
-                @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
-                @"(\[.*\])"
+                @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+                @"^(?<cleaned>.+?)(\[.*\])",
+                @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
+                @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
+                @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
             };
             };
 
 
             SubtitleFileExtensions = new[]
             SubtitleFileExtensions = new[]
@@ -250,6 +256,8 @@ namespace Emby.Naming.Common
                 },
                 },
                 // <!-- foo.ep01, foo.EP_01 -->
                 // <!-- foo.ep01, foo.EP_01 -->
                 new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
                 new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
+                // <!-- foo.E01., foo.e01. -->
+                new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
                 new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
                 new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
                 {
                 {
                     DateTimeFormats = new[]
                     DateTimeFormats = new[]
@@ -277,14 +285,14 @@ namespace Emby.Naming.Common
                     IsNamed = true
                     IsNamed = true
                 },
                 },
 
 
-                new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
+                new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$")
                 {
                 {
                     SupportsAbsoluteEpisodeNumbers = true
                     SupportsAbsoluteEpisodeNumbers = true
                 },
                 },
 
 
                 // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
                 // Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
                 // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
                 // [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
-                new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
+                new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
                 },
                 },
@@ -305,6 +313,12 @@ namespace Emby.Naming.Common
 
 
                 // *** End Kodi Standard Naming
                 // *** End Kodi Standard Naming
 
 
+                // "Episode 16", "Episode 16 - Title"
+                new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
+                {
+                    IsNamed = true
+                },
+
                 new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
                 {
                 {
                     IsNamed = true
                     IsNamed = true
@@ -362,12 +376,20 @@ namespace Emby.Naming.Common
                     IsOptimistic = true,
                     IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
                 },
                 },
-                // "Episode 16", "Episode 16 - Title"
-                new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
+
+                // 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]+)")
                 {
                 {
-                    IsOptimistic = true,
                     IsNamed = true
                     IsNamed = true
-                }
+                },
             };
             };
 
 
             EpisodeWithoutSeasonExpressions = new[]
             EpisodeWithoutSeasonExpressions = new[]
@@ -382,6 +404,12 @@ namespace Emby.Naming.Common
 
 
             VideoExtraRules = new[]
             VideoExtraRules = new[]
             {
             {
+                new ExtraRule(
+                    ExtraType.Trailer,
+                    ExtraRuleType.DirectoryName,
+                    "trailers",
+                    MediaType.Video),
+
                 new ExtraRule(
                 new ExtraRule(
                     ExtraType.Trailer,
                     ExtraType.Trailer,
                     ExtraRuleType.Filename,
                     ExtraRuleType.Filename,
@@ -448,6 +476,12 @@ namespace Emby.Naming.Common
                     "theme",
                     "theme",
                     MediaType.Audio),
                     MediaType.Audio),
 
 
+                new ExtraRule(
+                    ExtraType.ThemeSong,
+                    ExtraRuleType.DirectoryName,
+                    "theme-music",
+                    MediaType.Audio),
+
                 new ExtraRule(
                 new ExtraRule(
                     ExtraType.Scene,
                     ExtraType.Scene,
                     ExtraRuleType.Suffix,
                     ExtraRuleType.Suffix,
@@ -478,6 +512,12 @@ namespace Emby.Naming.Common
                     "-deleted",
                     "-deleted",
                     MediaType.Video),
                     MediaType.Video),
 
 
+                new ExtraRule(
+                    ExtraType.DeletedScene,
+                    ExtraRuleType.Suffix,
+                    "-deletedscene",
+                    MediaType.Video),
+
                 new ExtraRule(
                 new ExtraRule(
                     ExtraType.Clip,
                     ExtraType.Clip,
                     ExtraRuleType.Suffix,
                     ExtraRuleType.Suffix,
@@ -536,7 +576,7 @@ namespace Emby.Naming.Common
                     ExtraType.Unknown,
                     ExtraType.Unknown,
                     ExtraRuleType.DirectoryName,
                     ExtraRuleType.DirectoryName,
                     "extras",
                     "extras",
-                    MediaType.Video),
+                    MediaType.Video)
             };
             };
 
 
             Format3DRules = new[]
             Format3DRules = new[]
@@ -648,9 +688,29 @@ namespace Emby.Naming.Common
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .ToArray();
                 .ToArray();
 
 
+            AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["trailers"] = ExtraType.Trailer,
+                ["theme-music"] = ExtraType.ThemeSong,
+                ["backdrops"] = ExtraType.ThemeVideo,
+                ["extras"] = ExtraType.Unknown,
+                ["behind the scenes"] = ExtraType.BehindTheScenes,
+                ["deleted scenes"] = ExtraType.DeletedScene,
+                ["interviews"] = ExtraType.Interview,
+                ["scenes"] = ExtraType.Scene,
+                ["samples"] = ExtraType.Sample,
+                ["shorts"] = ExtraType.Clip,
+                ["featurettes"] = ExtraType.Clip
+            };
+
             Compile();
             Compile();
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the folder name to extra types mapping.
+        /// </summary>
+        public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets list of audio file extensions.
         /// Gets or sets list of audio file extensions.
         /// </summary>
         /// </summary>
@@ -732,9 +792,9 @@ namespace Emby.Naming.Common
         public Format3DRule[] Format3DRules { get; set; }
         public Format3DRule[] Format3DRules { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets list of raw video file-stacking expressions strings.
+        /// Gets the file stacking rules.
         /// </summary>
         /// </summary>
-        public string[] VideoFileStackingExpressions { get; set; }
+        public FileStackRule[] VideoFileStackingRules { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets list of raw clean DateTimes regular expressions strings.
         /// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -756,11 +816,6 @@ namespace Emby.Naming.Common
         /// </summary>
         /// </summary>
         public ExtraRule[] VideoExtraRules { get; set; }
         public ExtraRule[] VideoExtraRules { get; set; }
 
 
-        /// <summary>
-        /// Gets list of video file-stack regular expressions.
-        /// </summary>
-        public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
-
         /// <summary>
         /// <summary>
         /// Gets list of clean datetime regular expressions.
         /// Gets list of clean datetime regular expressions.
         /// </summary>
         /// </summary>
@@ -786,7 +841,6 @@ namespace Emby.Naming.Common
         /// </summary>
         /// </summary>
         public void Compile()
         public void Compile()
         {
         {
-            VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
             CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
             CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
             CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
             CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
             EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
             EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();

+ 3 - 9
Emby.Naming/Emby.Naming.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
 
   <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
   <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
   <PropertyGroup>
   <PropertyGroup>
@@ -6,15 +6,13 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <IncludeSymbols>true</IncludeSymbols>
     <IncludeSymbols>true</IncludeSymbols>
     <SymbolPackageFormat>snupkg</SymbolPackageFormat>
     <SymbolPackageFormat>snupkg</SymbolPackageFormat>
-    <Nullable>enable</Nullable>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
   <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -40,7 +38,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
@@ -50,8 +48,4 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
 </Project>
 </Project>

+ 6 - 5
Emby.Naming/Subtitles/SubtitleParser.cs

@@ -2,6 +2,7 @@ using System;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.Subtitles
 namespace Emby.Naming.Subtitles
 {
 {
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
             }
             }
 
 
             var extension = Path.GetExtension(path);
             var extension = Path.GetExtension(path);
-            if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return null;
                 return null;
             }
             }
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
             var flags = GetFlags(path);
             var flags = GetFlags(path);
             var info = new SubtitleInfo(
             var info = new SubtitleInfo(
                 path,
                 path,
-                _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
-                _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
+                _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
+                _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
 
 
-            var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
-                && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
+            var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
+                && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
                 .ToList();
                 .ToList();
 
 
             // Should have a name, language and file extension
             // Should have a name, language and file extension

+ 2 - 2
Emby.Naming/TV/EpisodeResolver.cs

@@ -1,8 +1,8 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
 using Emby.Naming.Video;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.TV
 namespace Emby.Naming.TV
 {
 {
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
             {
             {
                 var extension = Path.GetExtension(path);
                 var extension = Path.GetExtension(path);
                 // Check supported extensions
                 // Check supported extensions
-                if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+                if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     // It's not supported. Check stub extensions
                     // It's not supported. Check stub extensions
                     if (!StubResolver.TryResolveFile(path, _options, out stubType))
                     if (!StubResolver.TryResolveFile(path, _options, out stubType))

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

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

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

+ 13 - 13
Emby.Naming/Video/CleanStringParser.cs

@@ -1,4 +1,3 @@
-using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
@@ -17,38 +16,39 @@ namespace Emby.Naming.Video
         /// <param name="expressions">List of regex to parse name and year from.</param>
         /// <param name="expressions">List of regex to parse name and year from.</param>
         /// <param name="newName">Parsing result string.</param>
         /// <param name="newName">Parsing result string.</param>
         /// <returns>True if parsing was successful.</returns>
         /// <returns>True if parsing was successful.</returns>
-        public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
+        public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out string newName)
         {
         {
             if (string.IsNullOrEmpty(name))
             if (string.IsNullOrEmpty(name))
             {
             {
-                newName = ReadOnlySpan<char>.Empty;
+                newName = string.Empty;
                 return false;
                 return false;
             }
             }
 
 
-            var len = expressions.Count;
-            for (int i = 0; i < len; i++)
+            // Iteratively apply the regexps to clean the string.
+            bool cleaned = false;
+            for (int i = 0; i < expressions.Count; i++)
             {
             {
                 if (TryClean(name, expressions[i], out newName))
                 if (TryClean(name, expressions[i], out newName))
                 {
                 {
-                    return true;
+                    cleaned = true;
+                    name = newName;
                 }
                 }
             }
             }
 
 
-            newName = ReadOnlySpan<char>.Empty;
-            return false;
+            newName = cleaned ? name : string.Empty;
+            return cleaned;
         }
         }
 
 
-        private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
+        private static bool TryClean(string name, Regex expression, out string newName)
         {
         {
             var match = expression.Match(name);
             var match = expression.Match(name);
-            int index = match.Index;
-            if (match.Success && index != 0)
+            if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
             {
             {
-                newName = name.AsSpan().Slice(0, match.Index);
+                newName = cleaned.Value;
                 return true;
                 return true;
             }
             }
 
 
-            newName = ReadOnlySpan<char>.Empty;
+            newName = string.Empty;
             return false;
             return false;
         }
         }
     }
     }

+ 76 - 30
Emby.Naming/Video/ExtraResolver.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
@@ -10,44 +11,27 @@ namespace Emby.Naming.Video
     /// <summary>
     /// <summary>
     /// Resolve if file is extra for video.
     /// Resolve if file is extra for video.
     /// </summary>
     /// </summary>
-    public class ExtraResolver
+    public static class ExtraResolver
     {
     {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ExtraResolver"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
-        public ExtraResolver(NamingOptions options)
-        {
-            _options = options;
-        }
+        private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
 
 
         /// <summary>
         /// <summary>
         /// Attempts to resolve if file is extra.
         /// Attempts to resolve if file is extra.
         /// </summary>
         /// </summary>
         /// <param name="path">Path to file.</param>
         /// <param name="path">Path to file.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
-        public ExtraResult GetExtraInfo(string path)
+        public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
         {
         {
             var result = new ExtraResult();
             var result = new ExtraResult();
 
 
-            for (var i = 0; i < _options.VideoExtraRules.Length; i++)
+            for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
             {
             {
-                var rule = _options.VideoExtraRules[i];
-                if (rule.MediaType == MediaType.Audio)
+                var rule = namingOptions.VideoExtraRules[i];
+                if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+                    || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
                 {
                 {
-                    if (!AudioFileParser.IsAudioFile(path, _options))
-                    {
-                        continue;
-                    }
-                }
-                else if (rule.MediaType == MediaType.Video)
-                {
-                    if (!VideoResolver.IsVideoFile(path, _options))
-                    {
-                        continue;
-                    }
+                    continue;
                 }
                 }
 
 
                 var pathSpan = path.AsSpan();
                 var pathSpan = path.AsSpan();
@@ -63,9 +47,10 @@ namespace Emby.Naming.Video
                 }
                 }
                 else if (rule.RuleType == ExtraRuleType.Suffix)
                 else if (rule.RuleType == ExtraRuleType.Suffix)
                 {
                 {
-                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
+                    // Trim the digits from the end of the filename so we can recognize things like -trailer2
+                    var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
 
 
-                    if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
                     {
                     {
                         result.ExtraType = rule.ExtraType;
                         result.ExtraType = rule.ExtraType;
                         result.Rule = rule;
                         result.Rule = rule;
@@ -75,9 +60,9 @@ namespace Emby.Naming.Video
                 {
                 {
                     var filename = Path.GetFileName(path);
                     var filename = Path.GetFileName(path);
 
 
-                    var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+                    var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
 
-                    if (regex.IsMatch(filename))
+                    if (isMatch)
                     {
                     {
                         result.ExtraType = rule.ExtraType;
                         result.ExtraType = rule.ExtraType;
                         result.Rule = rule;
                         result.Rule = rule;
@@ -101,5 +86,66 @@ namespace Emby.Naming.Video
 
 
             return result;
             return result;
         }
         }
+
+        /// <summary>
+        /// Finds extras matching the video info.
+        /// </summary>
+        /// <param name="files">The list of file video infos.</param>
+        /// <param name="videoInfo">The video to compare against.</param>
+        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+        /// <returns>A list of video extras for [videoInfo].</returns>
+        public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
+
+            var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
+            var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
+
+            var result = new List<VideoFileInfo>();
+            for (var pos = files.Count - 1; pos >= 0; pos--)
+            {
+                var current = files[pos];
+                // ignore non-extras and multi-file (can this happen?)
+                if (current.ExtraType == null || current.Files.Count > 1)
+                {
+                    continue;
+                }
+
+                var currentFile = current.Files[0];
+                var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
+
+                // first check filenames
+                bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
+                               || (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
+
+                // then by directory
+                if (!isValid)
+                {
+                    // When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
+                    var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
+                        ? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
+                        : Path.GetDirectoryName(currentFile.Path.AsSpan());
+
+                    isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
+                }
+
+                if (isValid)
+                {
+                    result.Add(currentFile);
+                }
+            }
+
+            return result.OrderBy(r => r.Path).ToArray();
+        }
+
+        private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+        }
+
+        private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
+        {
+            return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
+        }
     }
     }
 }
 }

+ 17 - 12
Emby.Naming/Video/FileStack.cs

@@ -1,6 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Linq;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
 {
 {
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="FileStack"/> class.
         /// Initializes a new instance of the <see cref="FileStack"/> class.
         /// </summary>
         /// </summary>
-        public FileStack()
+        /// <param name="name">The stack name.</param>
+        /// <param name="isDirectory">Whether the stack files are directories.</param>
+        /// <param name="files">The stack files.</param>
+        public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
         {
         {
-            Files = new List<string>();
+            Name = name;
+            IsDirectoryStack = isDirectory;
+            Files = files;
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets name of file stack.
+        /// Gets the name of file stack.
         /// </summary>
         /// </summary>
-        public string Name { get; set; } = string.Empty;
+        public string Name { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets list of paths in stack.
+        /// Gets the list of paths in stack.
         /// </summary>
         /// </summary>
-        public List<string> Files { get; set; }
+        public IReadOnlyList<string> Files { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets a value indicating whether stack is directory stack.
+        /// Gets a value indicating whether stack is directory stack.
         /// </summary>
         /// </summary>
-        public bool IsDirectoryStack { get; set; }
+        public bool IsDirectoryStack { get; }
 
 
         /// <summary>
         /// <summary>
         /// Helper function to determine if path is in the stack.
         /// Helper function to determine if path is in the stack.
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
         /// <returns>True if file is in the stack.</returns>
         /// <returns>True if file is in the stack.</returns>
         public bool ContainsFile(string file, bool isDirectory)
         public bool ContainsFile(string file, bool isDirectory)
         {
         {
-            if (IsDirectoryStack == isDirectory)
+            if (string.IsNullOrEmpty(file))
             {
             {
-                return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
+                return false;
             }
             }
 
 
-            return false;
+            return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
         }
         }
     }
     }
 }
 }

+ 48 - 0
Emby.Naming/Video/FileStackRule.cs

@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+/// <summary>
+/// Regex based rule for file stacking (eg. disc1, disc2).
+/// </summary>
+public class FileStackRule
+{
+    private readonly Regex _tokenRegex;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="FileStackRule"/> class.
+    /// </summary>
+    /// <param name="token">Token.</param>
+    /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
+    public FileStackRule(string token, bool isNumerical)
+    {
+        _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+        IsNumerical = isNumerical;
+    }
+
+    /// <summary>
+    /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+    /// </summary>
+    public bool IsNumerical { get; }
+
+    /// <summary>
+    /// Match the input against the rule regex.
+    /// </summary>
+    /// <param name="input">The input.</param>
+    /// <param name="result">The part type and number or <c>null</c>.</param>
+    /// <returns>A value indicating whether the input matched the rule.</returns>
+    public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+    {
+        result = null;
+        var match = _tokenRegex.Match(input);
+        if (!match.Success)
+        {
+            return false;
+        }
+
+        var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
+        result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+        return true;
+    }
+}

+ 68 - 151
Emby.Naming/Video/StackResolver.cs

@@ -2,7 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Text.RegularExpressions;
 using Emby.Naming.AudioBook;
 using Emby.Naming.AudioBook;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
     /// <summary>
     /// <summary>
     /// Resolve <see cref="FileStack"/> from list of paths.
     /// Resolve <see cref="FileStack"/> from list of paths.
     /// </summary>
     /// </summary>
-    public class StackResolver
+    public static class StackResolver
     {
     {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StackResolver"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
-        public StackResolver(NamingOptions options)
-        {
-            _options = options;
-        }
-
         /// <summary>
         /// <summary>
         /// Resolves only directories from paths.
         /// Resolves only directories from paths.
         /// </summary>
         /// </summary>
         /// <param name="files">List of paths.</param>
         /// <param name="files">List of paths.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
         /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
-        public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
+        public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
         {
         {
-            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Resolves only files from paths.
         /// Resolves only files from paths.
         /// </summary>
         /// </summary>
         /// <param name="files">List of paths.</param>
         /// <param name="files">List of paths.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
         /// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
-        public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
+        public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
         {
         {
-            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
         /// </summary>
         /// </summary>
         /// <param name="files">List of paths.</param>
         /// <param name="files">List of paths.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
         /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
-        public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
+        public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
         {
         {
             var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
             var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
 
 
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
                 {
                 {
                     foreach (var file in directory)
                     foreach (var file in directory)
                     {
                     {
-                        var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
-                        stack.Files.Add(file.Path);
+                        var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
                         yield return stack;
                         yield return stack;
                     }
                     }
                 }
                 }
                 else
                 else
                 {
                 {
-                    var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
-                    foreach (var file in directory)
-                    {
-                        stack.Files.Add(file.Path);
-                    }
-
+                    var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
                     yield return stack;
                     yield return stack;
                 }
                 }
             }
             }
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
         /// Resolves videos from paths.
         /// Resolves videos from paths.
         /// </summary>
         /// </summary>
         /// <param name="files">List of paths.</param>
         /// <param name="files">List of paths.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
         /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
-        public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
+        public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
         {
         {
-            var list = files
-                .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
-                .OrderBy(i => i.FullName)
-                .ToList();
-
-            var expressions = _options.VideoFileStackingRegexes;
+            var potentialFiles = files
+                .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
+                .OrderBy(i => i.FullName);
 
 
-            for (var i = 0; i < list.Count; i++)
+            var potentialStacks = new Dictionary<string, StackMetadata>();
+            foreach (var file in potentialFiles)
             {
             {
-                var offset = 0;
-
-                var file1 = list[i];
+                var name = file.Name;
+                if (string.IsNullOrEmpty(name))
+                {
+                    name = Path.GetFileName(file.FullName);
+                }
 
 
-                var expressionIndex = 0;
-                while (expressionIndex < expressions.Length)
+                for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
                 {
                 {
-                    var exp = expressions[expressionIndex];
-                    var stack = new FileStack();
+                    var rule = namingOptions.VideoFileStackingRules[i];
+                    if (!rule.Match(name, out var stackParsingResult))
+                    {
+                        continue;
+                    }
 
 
-                    // (Title)(Volume)(Ignore)(Extension)
-                    var match1 = FindMatch(file1, exp, offset);
+                    var stackName = stackParsingResult.Value.StackName;
+                    var partNumber = stackParsingResult.Value.PartNumber;
+                    var partType = stackParsingResult.Value.PartType;
 
 
-                    if (match1.Success)
+                    if (!potentialStacks.TryGetValue(stackName, out var stackResult))
                     {
                     {
-                        var title1 = match1.Groups["title"].Value;
-                        var volume1 = match1.Groups["volume"].Value;
-                        var ignore1 = match1.Groups["ignore"].Value;
-                        var extension1 = match1.Groups["extension"].Value;
+                        stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+                        potentialStacks[stackName] = stackResult;
+                    }
 
 
-                        var j = i + 1;
-                        while (j < list.Count)
+                    if (stackResult.Parts.Count > 0)
+                    {
+                        if (stackResult.IsDirectory != file.IsDirectory
+                            || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+                            || stackResult.ContainsPart(partNumber))
                         {
                         {
-                            var file2 = list[j];
-
-                            if (file1.IsDirectory != file2.IsDirectory)
-                            {
-                                j++;
-                                continue;
-                            }
-
-                            // (Title)(Volume)(Ignore)(Extension)
-                            var match2 = FindMatch(file2, exp, offset);
-
-                            if (match2.Success)
-                            {
-                                var title2 = match2.Groups[1].Value;
-                                var volume2 = match2.Groups[2].Value;
-                                var ignore2 = match2.Groups[3].Value;
-                                var extension2 = match2.Groups[4].Value;
-
-                                if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
-                                {
-                                    if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
-                                            && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
-                                        {
-                                            if (stack.Files.Count == 0)
-                                            {
-                                                stack.Name = title1 + ignore1;
-                                                stack.IsDirectoryStack = file1.IsDirectory;
-                                                stack.Files.Add(file1.FullName);
-                                            }
-
-                                            stack.Files.Add(file2.FullName);
-                                        }
-                                        else
-                                        {
-                                            // Sequel
-                                            offset = 0;
-                                            expressionIndex++;
-                                            break;
-                                        }
-                                    }
-                                    else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        // False positive, try again with offset
-                                        offset = match1.Groups[3].Index;
-                                        break;
-                                    }
-                                    else
-                                    {
-                                        // Extension mismatch
-                                        offset = 0;
-                                        expressionIndex++;
-                                        break;
-                                    }
-                                }
-                                else
-                                {
-                                    // Title mismatch
-                                    offset = 0;
-                                    expressionIndex++;
-                                    break;
-                                }
-                            }
-                            else
-                            {
-                                // No match 2, next expression
-                                offset = 0;
-                                expressionIndex++;
-                                break;
-                            }
-
-                            j++;
+                            continue;
                         }
                         }
 
 
-                        if (j == list.Count)
+                        if (rule.IsNumerical != stackResult.IsNumerical)
                         {
                         {
-                            expressionIndex = expressions.Length;
+                            break;
                         }
                         }
                     }
                     }
-                    else
-                    {
-                        // No match 1
-                        offset = 0;
-                        expressionIndex++;
-                    }
 
 
-                    if (stack.Files.Count > 1)
-                    {
-                        yield return stack;
-                        i += stack.Files.Count - 1;
-                        break;
-                    }
+                    stackResult.Parts.Add(partNumber, file);
+                    break;
                 }
                 }
             }
             }
-        }
 
 
-        private static string GetRegexInput(FileSystemMetadata file)
-        {
-            // For directories, dummy up an extension otherwise the expressions will fail
-            var input = !file.IsDirectory
-                ? file.FullName
-                : file.FullName + ".mkv";
+            foreach (var (fileName, stack) in potentialStacks)
+            {
+                if (stack.Parts.Count < 2)
+                {
+                    continue;
+                }
 
 
-            return Path.GetFileName(input);
+                yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+            }
         }
         }
 
 
-        private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
+        private class StackMetadata
         {
         {
-            var regexInput = GetRegexInput(input);
-
-            if (offset < 0 || offset >= regexInput.Length)
+            public StackMetadata(bool isDirectory, bool isNumerical, string partType)
             {
             {
-                return Match.Empty;
+                Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+                IsDirectory = isDirectory;
+                IsNumerical = isNumerical;
+                PartType = partType;
             }
             }
 
 
-            return regex.Match(regexInput, offset);
+            public Dictionary<string, FileSystemMetadata> Parts { get; }
+
+            public bool IsDirectory { get; }
+
+            public bool IsNumerical { get; }
+
+            public string PartType { get; }
+
+            public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
         }
         }
     }
     }
 }
 }

+ 2 - 2
Emby.Naming/Video/StubResolver.cs

@@ -1,7 +1,7 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
 {
 {
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
 
 
             var extension = Path.GetExtension(path);
             var extension = Path.GetExtension(path);
 
 
-            if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return false;
                 return false;
             }
             }

+ 6 - 7
Emby.Naming/Video/VideoInfo.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
 {
 {
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
             Name = name;
             Name = name;
 
 
             Files = Array.Empty<VideoFileInfo>();
             Files = Array.Empty<VideoFileInfo>();
-            Extras = Array.Empty<VideoFileInfo>();
             AlternateVersions = Array.Empty<VideoFileInfo>();
             AlternateVersions = Array.Empty<VideoFileInfo>();
         }
         }
 
 
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
         /// <value>The files.</value>
         /// <value>The files.</value>
         public IReadOnlyList<VideoFileInfo> Files { get; set; }
         public IReadOnlyList<VideoFileInfo> Files { get; set; }
 
 
-        /// <summary>
-        /// Gets or sets the extras.
-        /// </summary>
-        /// <value>The extras.</value>
-        public IReadOnlyList<VideoFileInfo> Extras { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the alternate versions.
         /// Gets or sets the alternate versions.
         /// </summary>
         /// </summary>
         /// <value>The alternate versions.</value>
         /// <value>The alternate versions.</value>
         public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
         public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the extra type.
+        /// </summary>
+        public ExtraType? ExtraType { get; set; }
     }
     }
 }
 }

+ 30 - 144
Emby.Naming/Video/VideoListResolver.cs

@@ -4,7 +4,6 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
@@ -17,29 +16,38 @@ namespace Emby.Naming.Video
         /// <summary>
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
         /// Resolves alternative versions and extras from list of video files.
         /// </summary>
         /// </summary>
-        /// <param name="files">List of related video files.</param>
+        /// <param name="videoInfos">List of related video files.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
+        /// <param name="parseName">Whether to parse the name or use the filename.</param>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
-        public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
+        public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
         {
         {
-            var videoInfos = files
-                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
-                .OfType<VideoFileInfo>()
-                .ToList();
-
             // Filter out all extras, otherwise they could cause stacks to not be resolved
             // Filter out all extras, otherwise they could cause stacks to not be resolved
             // See the unit test TestStackedWithTrailer
             // See the unit test TestStackedWithTrailer
             var nonExtras = videoInfos
             var nonExtras = videoInfos
                 .Where(i => i.ExtraType == null)
                 .Where(i => i.ExtraType == null)
                 .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
                 .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
 
 
-            var stackResult = new StackResolver(namingOptions)
-                .Resolve(nonExtras).ToList();
+            var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
 
 
-            var remainingFiles = videoInfos
-                .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
-                .ToList();
+            var remainingFiles = new List<VideoFileInfo>();
+            var standaloneMedia = new List<VideoFileInfo>();
+
+            for (var i = 0; i < videoInfos.Count; i++)
+            {
+                var current = videoInfos[i];
+                if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+                {
+                    continue;
+                }
+
+                remainingFiles.Add(current);
+                if (current.ExtraType == null)
+                {
+                    standaloneMedia.Add(current);
+                }
+            }
 
 
             var list = new List<VideoInfo>();
             var list = new List<VideoInfo>();
 
 
@@ -47,27 +55,15 @@ namespace Emby.Naming.Video
             {
             {
                 var info = new VideoInfo(stack.Name)
                 var info = new VideoInfo(stack.Name)
                 {
                 {
-                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
                         .OfType<VideoFileInfo>()
                         .OfType<VideoFileInfo>()
                         .ToList()
                         .ToList()
                 };
                 };
 
 
                 info.Year = info.Files[0].Year;
                 info.Year = info.Files[0].Year;
-
-                var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
-
-                if (extras.Count > 0)
-                {
-                    info.Extras = extras;
-                }
-
                 list.Add(info);
                 list.Add(info);
             }
             }
 
 
-            var standaloneMedia = remainingFiles
-                .Where(i => i.ExtraType == null)
-                .ToList();
-
             foreach (var media in standaloneMedia)
             foreach (var media in standaloneMedia)
             {
             {
                 var info = new VideoInfo(media.Name) { Files = new[] { media } };
                 var info = new VideoInfo(media.Name) { Files = new[] { media } };
@@ -75,10 +71,6 @@ namespace Emby.Naming.Video
                 info.Year = info.Files[0].Year;
                 info.Year = info.Files[0].Year;
 
 
                 remainingFiles.Remove(media);
                 remainingFiles.Remove(media);
-                var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
-
-                info.Extras = extras;
-
                 list.Add(info);
                 list.Add(info);
             }
             }
 
 
@@ -87,58 +79,12 @@ namespace Emby.Naming.Video
                 list = GetVideosGroupedByVersion(list, namingOptions);
                 list = GetVideosGroupedByVersion(list, namingOptions);
             }
             }
 
 
-            // If there's only one resolved video, use the folder name as well to find extras
-            if (list.Count == 1)
-            {
-                var info = list[0];
-                var videoPath = list[0].Files[0].Path;
-                var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
-
-                if (!parentPath.IsEmpty)
-                {
-                    var folderName = Path.GetFileName(parentPath);
-                    if (!folderName.IsEmpty)
-                    {
-                        var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
-                        extras.AddRange(info.Extras);
-                        info.Extras = extras;
-                    }
-                }
-
-                // Add the extras that are just based on file name as well
-                var extrasByFileName = remainingFiles
-                    .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
-                    .ToList();
-
-                remainingFiles = remainingFiles
-                    .Except(extrasByFileName)
-                    .ToList();
-
-                extrasByFileName.AddRange(info.Extras);
-                info.Extras = extrasByFileName;
-            }
-
-            // If there's only one video, accept all trailers
-            // Be lenient because people use all kinds of mishmash conventions with trailers.
-            if (list.Count == 1)
-            {
-                var trailers = remainingFiles
-                    .Where(i => i.ExtraType == ExtraType.Trailer)
-                    .ToList();
-
-                trailers.AddRange(list[0].Extras);
-                list[0].Extras = trailers;
-
-                remainingFiles = remainingFiles
-                    .Except(trailers)
-                    .ToList();
-            }
-
             // Whatever files are left, just add them
             // Whatever files are left, just add them
             list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
             list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
             {
             {
                 Files = new[] { i },
                 Files = new[] { i },
-                Year = i.Year
+                Year = i.Year,
+                ExtraType = i.ExtraType
             }));
             }));
 
 
             return list;
             return list;
@@ -162,6 +108,11 @@ namespace Emby.Naming.Video
             for (var i = 0; i < videos.Count; i++)
             for (var i = 0; i < videos.Count; i++)
             {
             {
                 var video = videos[i];
                 var video = videos[i];
+                if (video.ExtraType != null)
+                {
+                    continue;
+                }
+
                 if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
                 if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
                 {
                 {
                     return videos;
                     return videos;
@@ -178,17 +129,14 @@ namespace Emby.Naming.Video
 
 
             var alternateVersionsLen = videos.Count - 1;
             var alternateVersionsLen = videos.Count - 1;
             var alternateVersions = new VideoFileInfo[alternateVersionsLen];
             var alternateVersions = new VideoFileInfo[alternateVersionsLen];
-            var extras = new List<VideoFileInfo>(list[0].Extras);
             for (int i = 0; i < alternateVersionsLen; i++)
             for (int i = 0; i < alternateVersionsLen; i++)
             {
             {
                 var video = videos[i + 1];
                 var video = videos[i + 1];
                 alternateVersions[i] = video.Files[0];
                 alternateVersions[i] = video.Files[0];
-                extras.AddRange(video.Extras);
             }
             }
 
 
             list[0].AlternateVersions = alternateVersions;
             list[0].AlternateVersions = alternateVersions;
             list[0].Name = folderName.ToString();
             list[0].Name = folderName.ToString();
-            list[0].Extras = extras;
 
 
             return list;
             return list;
         }
         }
@@ -230,7 +178,7 @@ namespace Emby.Naming.Video
             var tmpTestFilename = testFilename.ToString();
             var tmpTestFilename = testFilename.ToString();
             if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
             if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
             {
             {
-                tmpTestFilename = cleanName.Trim().ToString();
+                tmpTestFilename = cleanName.Trim();
             }
             }
 
 
             // The CleanStringParser should have removed common keywords etc.
             // The CleanStringParser should have removed common keywords etc.
@@ -238,67 +186,5 @@ namespace Emby.Naming.Video
                    || testFilename[0] == '-'
                    || testFilename[0] == '-'
                    || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
                    || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
         }
         }
-
-        private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
-        {
-            return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
-        }
-
-        private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
-        {
-            if (baseName.IsEmpty)
-            {
-                return false;
-            }
-
-            return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
-                   || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
-        }
-
-        /// <summary>
-        /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
-        /// </summary>
-        /// <param name="remainingFiles">The list of remaining filenames.</param>
-        /// <param name="baseName">The base name to use for the comparison.</param>
-        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
-        /// <returns>A list of video extras for [baseName].</returns>
-        private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
-        {
-            return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
-        }
-
-        /// <summary>
-        /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
-        /// </summary>
-        /// <param name="remainingFiles">The list of remaining filenames.</param>
-        /// <param name="firstBaseName">The first base name to use for the comparison.</param>
-        /// <param name="secondBaseName">The second base name to use for the comparison.</param>
-        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
-        /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
-        private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
-        {
-            var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
-            var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
-
-            var result = new List<VideoFileInfo>();
-            for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
-            {
-                var file = remainingFiles[pos];
-                if (file.ExtraType == null)
-                {
-                    continue;
-                }
-
-                var filename = file.FileNameWithoutExtension;
-                if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
-                    || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
-                {
-                    result.Add(file);
-                    remainingFiles.RemoveAt(pos);
-                }
-            }
-
-            return result;
-        }
     }
     }
 }
 }

+ 8 - 7
Emby.Naming/Video/VideoResolver.cs

@@ -2,7 +2,7 @@ using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.IO;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
 {
 {
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
+        /// <param name="parseName">Whether to parse the name or use the filename.</param>
         /// <returns>VideoFileInfo.</returns>
         /// <returns>VideoFileInfo.</returns>
-        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
+        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
         {
         {
-            return Resolve(path, true, namingOptions);
+            return Resolve(path, true, namingOptions, parseName);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
 
 
             var format3DResult = Format3DParser.Parse(path, namingOptions);
             var format3DResult = Format3DParser.Parse(path, namingOptions);
 
 
-            var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
+            var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
 
 
             var name = Path.GetFileNameWithoutExtension(path);
             var name = Path.GetFileNameWithoutExtension(path);
 
 
@@ -87,9 +88,9 @@ namespace Emby.Naming.Video
                 year = cleanDateTimeResult.Year;
                 year = cleanDateTimeResult.Year;
 
 
                 if (extraResult.ExtraType == null
                 if (extraResult.ExtraType == null
-                    && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
+                    && TryCleanString(name, namingOptions, out var newName))
                 {
                 {
-                    name = newName.ToString();
+                    name = newName;
                 }
                 }
             }
             }
 
 
@@ -138,7 +139,7 @@ namespace Emby.Naming.Video
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="newName">Clean name.</param>
         /// <param name="newName">Clean name.</param>
         /// <returns>True if cleaning of name was successful.</returns>
         /// <returns>True if cleaning of name was successful.</returns>
-        public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
+        public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
         {
         {
             return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
             return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
         }
         }

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

@@ -6,13 +6,9 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-    <Nullable>enable</Nullable>
-    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 2 - 22
Emby.Notifications/NotificationEntryPoint.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -77,7 +78,6 @@ namespace Emby.Notifications
         {
         {
             _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
             _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
             _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
             _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
-            _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
             _activityManager.EntryCreated += OnActivityManagerEntryCreated;
             _activityManager.EntryCreated += OnActivityManagerEntryCreated;
 
 
             return Task.CompletedTask;
             return Task.CompletedTask;
@@ -105,7 +105,7 @@ namespace Emby.Notifications
 
 
             var type = entry.Type;
             var type = entry.Type;
 
 
-            if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+            if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return;
                 return;
             }
             }
@@ -132,25 +132,6 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
         }
 
 
-        private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
-        {
-            if (!_appHost.HasUpdateAvailable)
-            {
-                return;
-            }
-
-            var type = NotificationType.ApplicationUpdateAvailable.ToString();
-
-            var notification = new NotificationRequest
-            {
-                Description = "Please see jellyfin.org for details.",
-                NotificationType = type,
-                Name = _localization.GetLocalizedString("NewVersionIsAvailable")
-            };
-
-            await SendNotification(notification, null).ConfigureAwait(false);
-        }
-
         private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
         private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
         {
         {
             if (!FilterItem(e.Item))
             if (!FilterItem(e.Item))
@@ -325,7 +306,6 @@ namespace Emby.Notifications
 
 
             _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
             _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
             _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
             _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
-            _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
             _activityManager.EntryCreated -= OnActivityManagerEntryCreated;
             _activityManager.EntryCreated -= OnActivityManagerEntryCreated;
 
 
             _disposed = true;
             _disposed = true;

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

@@ -19,13 +19,9 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-    <Nullable>enable</Nullable>
-    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

+ 2 - 1
Emby.Photos/PhotoProvider.cs

@@ -3,6 +3,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -60,7 +61,7 @@ namespace Emby.Photos
             item.SetImagePath(ImageType.Primary, item.Path);
             item.SetImagePath(ImageType.Primary, item.Path);
 
 
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
-            if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
+            if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
             {
             {
                 try
                 try
                 {
                 {

+ 1 - 1
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
         {
         {
             return _configurations.GetOrAdd(
             return _configurations.GetOrAdd(
                 key,
                 key,
-                (k, configurationManager) =>
+                static (k, configurationManager) =>
                 {
                 {
                     var file = configurationManager.GetConfigurationFile(k);
                     var file = configurationManager.GetConfigurationFile(k);
 
 

+ 4 - 6
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
-using System.Linq;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 
 
 namespace Emby.Server.Implementations.AppBase
 namespace Emby.Server.Implementations.AppBase
@@ -41,20 +40,19 @@ namespace Emby.Server.Implementations.AppBase
             xmlSerializer.SerializeToStream(configuration, stream);
             xmlSerializer.SerializeToStream(configuration, stream);
 
 
             // Take the object we just got and serialize it back to bytes
             // Take the object we just got and serialize it back to bytes
-            byte[] newBytes = stream.GetBuffer();
-            int newBytesLen = (int)stream.Length;
+            Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
 
 
             // If the file didn't exist before, or if something has changed, re-save
             // If the file didn't exist before, or if something has changed, re-save
-            if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
+            if (buffer == null || !newBytes.SequenceEqual(buffer))
             {
             {
                 var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
                 var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
 
 
                 Directory.CreateDirectory(directory);
                 Directory.CreateDirectory(directory);
+
                 // Save it after load in case we got new items
                 // Save it after load in case we got new items
-                // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
                 using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
                 using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
                 {
                 {
-                    fs.Write(newBytes, 0, newBytesLen);
+                    fs.Write(newBytes);
                 }
                 }
             }
             }
 
 

File diff suppressed because it is too large
+ 236 - 379
Emby.Server.Implementations/ApplicationHost.cs


+ 1 - 112
Emby.Server.Implementations/Archiving/ZipClient.cs

@@ -1,11 +1,8 @@
 using System.IO;
 using System.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
-using SharpCompress.Archives.SevenZip;
-using SharpCompress.Archives.Tar;
 using SharpCompress.Common;
 using SharpCompress.Common;
 using SharpCompress.Readers;
 using SharpCompress.Readers;
 using SharpCompress.Readers.GZip;
 using SharpCompress.Readers.GZip;
-using SharpCompress.Readers.Zip;
 
 
 namespace Emby.Server.Implementations.Archiving
 namespace Emby.Server.Implementations.Archiving
 {
 {
@@ -14,53 +11,6 @@ namespace Emby.Server.Implementations.Archiving
     /// </summary>
     /// </summary>
     public class ZipClient : IZipClient
     public class ZipClient : IZipClient
     {
     {
-        /// <summary>
-        /// Extracts all.
-        /// </summary>
-        /// <param name="sourceFile">The source file.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
-        {
-            using var fileStream = File.OpenRead(sourceFile);
-            ExtractAll(fileStream, targetPath, overwriteExistingFiles);
-        }
-
-        /// <summary>
-        /// Extracts all.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var reader = ReaderFactory.Open(source);
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true
-            };
-
-            if (overwriteExistingFiles)
-            {
-                options.Overwrite = true;
-            }
-
-            reader.WriteAllToDirectory(targetPath, options);
-        }
-
-        /// <inheritdoc />
-        public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var reader = ZipReader.Open(source);
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true,
-                Overwrite = overwriteExistingFiles
-            };
-
-            reader.WriteAllToDirectory(targetPath, options);
-        }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
         public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
         {
         {
@@ -71,6 +21,7 @@ namespace Emby.Server.Implementations.Archiving
                 Overwrite = overwriteExistingFiles
                 Overwrite = overwriteExistingFiles
             };
             };
 
 
+            Directory.CreateDirectory(targetPath);
             reader.WriteAllToDirectory(targetPath, options);
             reader.WriteAllToDirectory(targetPath, options);
         }
         }
 
 
@@ -91,67 +42,5 @@ namespace Emby.Server.Implementations.Archiving
                 reader.WriteEntryToFile(Path.Combine(targetPath, filename));
                 reader.WriteEntryToFile(Path.Combine(targetPath, filename));
             }
             }
         }
         }
-
-        /// <summary>
-        /// Extracts all from7z.
-        /// </summary>
-        /// <param name="sourceFile">The source file.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
-        {
-            using var fileStream = File.OpenRead(sourceFile);
-            ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
-        }
-
-        /// <summary>
-        /// Extracts all from7z.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var archive = SevenZipArchive.Open(source);
-            using var reader = archive.ExtractAllEntries();
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true,
-                Overwrite = overwriteExistingFiles
-            };
-
-            reader.WriteAllToDirectory(targetPath, options);
-        }
-
-        /// <summary>
-        /// Extracts all from tar.
-        /// </summary>
-        /// <param name="sourceFile">The source file.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
-        {
-            using var fileStream = File.OpenRead(sourceFile);
-            ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
-        }
-
-        /// <summary>
-        /// Extracts all from tar.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var archive = TarArchive.Open(source);
-            using var reader = archive.ExtractAllEntries();
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true,
-                Overwrite = overwriteExistingFiles
-            };
-
-            reader.WriteAllToDirectory(targetPath, options);
-        }
     }
     }
 }
 }

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

@@ -10,8 +10,9 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -102,7 +103,7 @@ namespace Emby.Server.Implementations.Channels
             var internalChannel = _libraryManager.GetItemById(item.ChannelId);
             var internalChannel = _libraryManager.GetItemById(item.ChannelId);
             var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
             var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
 
 
-            return !(channel is IDisableMediaSourceDisplay);
+            return channel is not IDisableMediaSourceDisplay;
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.Channels
                     try
                     try
                     {
                     {
                         return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
                         return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
-                            && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
+                            && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
                     }
                     }
                     catch
                     catch
                     {
                     {
@@ -541,7 +542,7 @@ namespace Emby.Server.Implementations.Channels
             return _libraryManager.GetItemIds(
             return _libraryManager.GetItemIds(
                 new InternalItemsQuery
                 new InternalItemsQuery
                 {
                 {
-                    IncludeItemTypes = new[] { nameof(Channel) },
+                    IncludeItemTypes = new[] { BaseItemKind.Channel },
                     OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
                     OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
                 }).Select(i => GetChannelFeatures(i)).ToArray();
                 }).Select(i => GetChannelFeatures(i)).ToArray();
         }
         }
@@ -586,7 +587,7 @@ namespace Emby.Server.Implementations.Channels
         {
         {
             var supportsLatest = provider is ISupportsLatestMedia;
             var supportsLatest = provider is ISupportsLatestMedia;
 
 
-            return new ChannelFeatures
+            return new ChannelFeatures(channel.Name, channel.Id)
             {
             {
                 CanFilter = !features.MaxPageSize.HasValue,
                 CanFilter = !features.MaxPageSize.HasValue,
                 CanSearch = provider is ISearchableChannel,
                 CanSearch = provider is ISearchableChannel,
@@ -596,8 +597,6 @@ namespace Emby.Server.Implementations.Channels
                 MediaTypes = features.MediaTypes.ToArray(),
                 MediaTypes = features.MediaTypes.ToArray(),
                 SupportsSortOrderToggle = features.SupportsSortOrderToggle,
                 SupportsSortOrderToggle = features.SupportsSortOrderToggle,
                 SupportsLatestMedia = supportsLatest,
                 SupportsLatestMedia = supportsLatest,
-                Name = channel.Name,
-                Id = channel.Id.ToString("N", CultureInfo.InvariantCulture),
                 SupportsContentDownloading = features.SupportsContentDownloading,
                 SupportsContentDownloading = features.SupportsContentDownloading,
                 AutoRefreshLevels = features.AutoRefreshLevels
                 AutoRefreshLevels = features.AutoRefreshLevels
             };
             };
@@ -815,7 +814,7 @@ namespace Emby.Server.Implementations.Channels
             {
             {
                 if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                 if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                 {
                 {
-                    await using FileStream jsonStream = File.OpenRead(cachePath);
+                    await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
                     var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                     var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                     if (cachedResult != null)
                     if (cachedResult != null)
                     {
                     {
@@ -838,7 +837,7 @@ namespace Emby.Server.Implementations.Channels
                 {
                 {
                     if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                     if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                     {
                     {
-                        await using FileStream jsonStream = File.OpenRead(cachePath);
+                        await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
                         var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                         var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                         if (cachedResult != null)
                         if (cachedResult != null)
                         {
                         {
@@ -880,7 +879,7 @@ namespace Emby.Server.Implementations.Channels
             }
             }
         }
         }
 
 
-        private async Task CacheResponse(object result, string path)
+        private async Task CacheResponse(ChannelItemResult result, string path)
         {
         {
             try
             try
             {
             {
@@ -1077,14 +1076,6 @@ namespace Emby.Server.Implementations.Channels
                 forceUpdate = true;
                 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))
             if (!internalChannelId.Equals(item.ChannelId))
             {
             {
                 forceUpdate = true;
                 forceUpdate = true;
@@ -1145,7 +1136,7 @@ namespace Emby.Server.Implementations.Channels
 
 
             if (!info.IsLiveStream)
             if (!info.IsLiveStream)
             {
             {
-                if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+                if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
                     item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
                     _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
                     _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
@@ -1154,7 +1145,7 @@ namespace Emby.Server.Implementations.Channels
             }
             }
             else
             else
             {
             {
-                if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+                if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
                     item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
                     _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
                     _logger.LogDebug("Forcing update due to Tags {0}", item.Name);

+ 2 - 1
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
 
 
             var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
             var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
             {
             {
-                IncludeItemTypes = new[] { nameof(Channel) },
+                IncludeItemTypes = new[] { BaseItemKind.Channel },
                 ExcludeItemIds = installedChannelIds.ToArray()
                 ExcludeItemIds = installedChannelIds.ToArray()
             });
             });
 
 

+ 20 - 28
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -63,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+        public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+        public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+        public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
 
 
         private IEnumerable<Folder> FindFolders(string path)
         private IEnumerable<Folder> FindFolders(string path)
         {
         {
@@ -80,14 +78,12 @@ namespace Emby.Server.Implementations.Collections
                 .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
                 .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
         }
         }
 
 
-        internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
+        internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
         {
         {
-            var existingFolders = FindFolders(path)
-                .ToList();
-
-            if (existingFolders.Count > 0)
+            var existingFolder = FindFolders(path).FirstOrDefault();
+            if (existingFolder != null)
             {
             {
-                return existingFolders[0];
+                return existingFolder;
             }
             }
 
 
             if (!createIfNeeded)
             if (!createIfNeeded)
@@ -99,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
 
 
             var libraryOptions = new LibraryOptions
             var libraryOptions = new LibraryOptions
             {
             {
-                PathInfos = new[] { new MediaPathInfo { Path = path } },
+                PathInfos = new[] { new MediaPathInfo(path) },
                 EnableRealtimeMonitor = false,
                 EnableRealtimeMonitor = false,
                 SaveLocalMetadata = true
                 SaveLocalMetadata = true
             };
             };
@@ -116,7 +112,7 @@ namespace Emby.Server.Implementations.Collections
             return Path.Combine(_appPaths.DataPath, "collections");
             return Path.Combine(_appPaths.DataPath, "collections");
         }
         }
 
 
-        private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
+        private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
         {
         {
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
         }
         }
@@ -164,7 +160,7 @@ namespace Emby.Server.Implementations.Collections
                     DateCreated = DateTime.UtcNow
                     DateCreated = DateTime.UtcNow
                 };
                 };
 
 
-                parentFolder.AddChild(collection, CancellationToken.None);
+                parentFolder.AddChild(collection);
 
 
                 if (options.ItemIdList.Count > 0)
                 if (options.ItemIdList.Count > 0)
                 {
                 {
@@ -200,13 +196,12 @@ namespace Emby.Server.Implementations.Collections
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
-            => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
+        public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
+            => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
 
 
         private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
         private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
         {
         {
-            var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-            if (collection == null)
+            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
             }
@@ -258,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
         /// <inheritdoc />
         /// <inheritdoc />
         public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
         public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
         {
         {
-            var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-
-            if (collection == null)
+            if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
             {
                 throw new ArgumentException("No collection exists with the supplied Id");
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
             }
@@ -314,11 +307,7 @@ namespace Emby.Server.Implementations.Collections
 
 
             foreach (var item in items)
             foreach (var item in items)
             {
             {
-                if (item is not ISupportsBoxSetGrouping)
-                {
-                    results[item.Id] = item;
-                }
-                else
+                if (item is ISupportsBoxSetGrouping)
                 {
                 {
                     var itemId = item.Id;
                     var itemId = item.Id;
 
 
@@ -342,6 +331,7 @@ namespace Emby.Server.Implementations.Collections
                     }
                     }
 
 
                     var alreadyInResults = false;
                     var alreadyInResults = false;
+
                     // this is kind of a performance hack because only Video has alternate versions that should be in a box set?
                     // this is kind of a performance hack because only Video has alternate versions that should be in a box set?
                     if (item is Video video)
                     if (item is Video video)
                     {
                     {
@@ -357,11 +347,13 @@ namespace Emby.Server.Implementations.Collections
                         }
                         }
                     }
                     }
 
 
-                    if (!alreadyInResults)
+                    if (alreadyInResults)
                     {
                     {
-                        results[itemId] = item;
+                        continue;
                     }
                     }
                 }
                 }
+
+                results[item.Id] = item;
             }
             }
 
 
             return results.Values;
             return results.Values;

+ 52 - 74
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -1,17 +1,20 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
+using System.Text;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.Cryptography.Constants;
+using static MediaBrowser.Model.Cryptography.Constants;
 
 
 namespace Emby.Server.Implementations.Cryptography
 namespace Emby.Server.Implementations.Cryptography
 {
 {
     /// <summary>
     /// <summary>
     /// Class providing abstractions over cryptographic functions.
     /// Class providing abstractions over cryptographic functions.
     /// </summary>
     /// </summary>
-    public class CryptographyProvider : ICryptoProvider, IDisposable
+    public class CryptographyProvider : ICryptoProvider
     {
     {
+        // TODO: remove when not needed for backwards compat
         private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
         private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
             {
             {
                 "MD5",
                 "MD5",
@@ -30,71 +33,71 @@ namespace Emby.Server.Implementations.Cryptography
                 "System.Security.Cryptography.SHA512"
                 "System.Security.Cryptography.SHA512"
             };
             };
 
 
-        private RandomNumberGenerator _randomNumberGenerator;
-
-        private bool _disposed;
+        /// <inheritdoc />
+        public string DefaultHashMethod => "PBKDF2-SHA512";
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CryptographyProvider"/> class.
-        /// </summary>
-        public CryptographyProvider()
+        /// <inheritdoc />
+        public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
         {
         {
-            // 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
-            _randomNumberGenerator = RandomNumberGenerator.Create();
+            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 />
         /// <inheritdoc />
-        public string DefaultHashMethod => "PBKDF2";
-
-        /// <inheritdoc />
-        public IEnumerable<string> GetSupportedHashMethods()
-            => _supportedHashMethods;
-
-        private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
+        public bool Verify(PasswordHash hash, 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)
+            if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
             {
             {
-                throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
+                return hash.Hash.SequenceEqual(
+                    Rfc2898DeriveBytes.Pbkdf2(
+                        password,
+                        hash.Salt,
+                        int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+                        HashAlgorithmName.SHA1,
+                        32));
             }
             }
 
 
-            using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
-            return r.GetBytes(32);
-        }
-
-        /// <inheritdoc />
-        public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
-        {
-            if (hashMethod == DefaultHashMethod)
+            if (string.Equals(hash.Id, "PBKDF2-SHA512", 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.SHA512,
+                        DefaultOutputLength));
             }
             }
 
 
-            if (!_supportedHashMethods.Contains(hashMethod))
+            if (!_supportedHashMethods.Contains(hash.Id))
             {
             {
-                throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+                throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
             }
             }
 
 
-            using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
-            if (salt.Length == 0)
+            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 h.ComputeHash(bytes);
+                return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
             }
             }
 
 
-            byte[] salted = new byte[bytes.Length + salt.Length];
+            byte[] salted = new byte[bytes.Length + hash.Salt.Length];
             Array.Copy(bytes, salted, bytes.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 />
         /// <inheritdoc />
         public byte[] GenerateSalt()
         public byte[] GenerateSalt()
             => GenerateSalt(DefaultSaltLength);
             => GenerateSalt(DefaultSaltLength);
@@ -102,35 +105,10 @@ namespace Emby.Server.Implementations.Cryptography
         /// <inheritdoc />
         /// <inheritdoc />
         public byte[] GenerateSalt(int length)
         public byte[] GenerateSalt(int length)
         {
         {
-            byte[] salt = new byte[length];
-            _randomNumberGenerator.GetBytes(salt);
+            var salt = new byte[length];
+            using var rng = RandomNumberGenerator.Create();
+            rng.GetNonZeroBytes(salt);
             return salt;
             return salt;
         }
         }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                _randomNumberGenerator.Dispose();
-            }
-
-            _disposed = true;
-        }
     }
     }
 }
 }

+ 4 - 55
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -4,8 +4,8 @@
 
 
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Linq;
 using System.Threading;
 using System.Threading;
+using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 using SQLitePCL.pretty;
 
 
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Data
         protected virtual int? CacheSize => null;
         protected virtual int? CacheSize => null;
 
 
         /// <summary>
         /// <summary>
-        /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />
+        /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
         /// </summary>
         /// </summary>
         /// <value>The journal mode.</value>
         /// <value>The journal mode.</value>
         protected virtual string JournalMode => "TRUNCATE";
         protected virtual string JournalMode => "TRUNCATE";
@@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
         /// <value>The write connection.</value>
         /// <value>The write connection.</value>
         protected SQLiteDatabaseConnection WriteConnection { get; set; }
         protected SQLiteDatabaseConnection WriteConnection { get; set; }
 
 
-        protected ManagedConnection GetConnection(bool _ = false)
+        protected ManagedConnection GetConnection(bool readOnly = false)
         {
         {
             WriteLock.Wait();
             WriteLock.Wait();
             if (WriteConnection != null)
             if (WriteConnection != null)
@@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Data
 
 
         protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
         protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
         {
         {
-            if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
+            if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return;
                 return;
             }
             }
@@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
             _disposed = true;
             _disposed = true;
         }
         }
     }
     }
-
-    /// <summary>
-    /// The disk synchronization mode, controls how aggressively SQLite will write data
-    /// all the way out to physical storage.
-    /// </summary>
-    public enum SynchronousMode
-    {
-        /// <summary>
-        /// SQLite continues without syncing as soon as it has handed data off to the operating system.
-        /// </summary>
-        Off = 0,
-
-        /// <summary>
-        /// SQLite database engine will still sync at the most critical moments.
-        /// </summary>
-        Normal = 1,
-
-        /// <summary>
-        /// SQLite database engine will use the xSync method of the VFS
-        /// to ensure that all content is safely written to the disk surface prior to continuing.
-        /// </summary>
-        Full = 2,
-
-        /// <summary>
-        /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
-        /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
-        /// </summary>
-        Extra = 3
-    }
-
-    /// <summary>
-    /// Storage mode used by temporary database files.
-    /// </summary>
-    public enum TempStoreMode
-    {
-        /// <summary>
-        /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
-        /// is used to determine where temporary tables and indices are stored.
-        /// </summary>
-        Default = 0,
-
-        /// <summary>
-        /// Temporary tables and indices are stored in a file.
-        /// </summary>
-        File = 1,
-
-        /// <summary>
-        /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
-        /// </summary>
-        Memory = 2
-    }
 }
 }

+ 4 - 2
Emby.Server.Implementations/Data/ManagedConnection.cs

@@ -7,10 +7,12 @@ using SQLitePCL.pretty;
 
 
 namespace Emby.Server.Implementations.Data
 namespace Emby.Server.Implementations.Data
 {
 {
-    public class ManagedConnection : IDisposable
+    public sealed class ManagedConnection : IDisposable
     {
     {
-        private SQLiteDatabaseConnection? _db;
         private readonly SemaphoreSlim _writeLock;
         private readonly SemaphoreSlim _writeLock;
+
+        private SQLiteDatabaseConnection? _db;
+
         private bool _disposed = false;
         private bool _disposed = false;
 
 
         public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
         public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)

+ 3 - 3
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
                 dateText,
                 dateText,
                 _datetimeFormats,
                 _datetimeFormats,
                 DateTimeFormatInfo.InvariantInfo,
                 DateTimeFormatInfo.InvariantInfo,
-                DateTimeStyles.None).ToUniversalTime();
+                DateTimeStyles.AdjustToUniversal);
         }
         }
 
 
         public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
         public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
 
 
             var dateText = item.ToString();
             var dateText = item.ToString();
 
 
-            if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
+            if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
             {
             {
-                result = dateTimeResult.ToUniversalTime();
+                result = dateTimeResult;
                 return true;
                 return true;
             }
             }
 
 

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


+ 31 - 29
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// <summary>
         /// Opens the connection to the database.
         /// Opens the connection to the database.
         /// </summary>
         /// </summary>
+        /// <param name="userManager">The user manager.</param>
+        /// <param name="dbLock">The lock to use for database IO.</param>
+        /// <param name="dbConnection">The connection to use for database IO.</param>
         public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
         public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
         {
         {
             WriteLock.Dispose();
             WriteLock.Dispose();
@@ -49,8 +52,8 @@ namespace Emby.Server.Implementations.Data
                 connection.RunInTransaction(
                 connection.RunInTransaction(
                 db =>
                 db =>
                 {
                 {
-                    db.ExecuteAll(string.Join(';', new[] {
-
+                    db.ExecuteAll(string.Join(';', new[]
+                    {
                         "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
                         "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
 
 
                         "drop index if exists idx_userdata",
                         "drop index if exists idx_userdata",
@@ -129,19 +132,17 @@ namespace Emby.Server.Implementations.Data
             return list;
             return list;
         }
         }
 
 
-        /// <summary>
-        /// Saves the user data.
-        /// </summary>
-        public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
         {
         {
             if (userData == null)
             if (userData == null)
             {
             {
                 throw new ArgumentNullException(nameof(userData));
                 throw new ArgumentNullException(nameof(userData));
             }
             }
 
 
-            if (internalUserId <= 0)
+            if (userId <= 0)
             {
             {
-                throw new ArgumentNullException(nameof(internalUserId));
+                throw new ArgumentNullException(nameof(userId));
             }
             }
 
 
             if (string.IsNullOrEmpty(key))
             if (string.IsNullOrEmpty(key))
@@ -149,22 +150,23 @@ namespace Emby.Server.Implementations.Data
                 throw new ArgumentNullException(nameof(key));
                 throw new ArgumentNullException(nameof(key));
             }
             }
 
 
-            PersistUserData(internalUserId, key, userData, cancellationToken);
+            PersistUserData(userId, key, userData, cancellationToken);
         }
         }
 
 
-        public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
         {
         {
             if (userData == null)
             if (userData == null)
             {
             {
                 throw new ArgumentNullException(nameof(userData));
                 throw new ArgumentNullException(nameof(userData));
             }
             }
 
 
-            if (internalUserId <= 0)
+            if (userId <= 0)
             {
             {
-                throw new ArgumentNullException(nameof(internalUserId));
+                throw new ArgumentNullException(nameof(userId));
             }
             }
 
 
-            PersistAllUserData(internalUserId, userData, cancellationToken);
+            PersistAllUserData(userId, userData, cancellationToken);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -174,7 +176,6 @@ namespace Emby.Server.Implementations.Data
         /// <param name="key">The key.</param>
         /// <param name="key">The key.</param>
         /// <param name="userData">The user data.</param>
         /// <param name="userData">The user data.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
         public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
         public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
         {
         {
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
@@ -264,19 +265,19 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// <summary>
         /// Gets the user data.
         /// Gets the user data.
         /// </summary>
         /// </summary>
-        /// <param name="internalUserId">The user id.</param>
+        /// <param name="userId">The user id.</param>
         /// <param name="key">The key.</param>
         /// <param name="key">The key.</param>
         /// <returns>Task{UserItemData}.</returns>
         /// <returns>Task{UserItemData}.</returns>
         /// <exception cref="ArgumentNullException">
         /// <exception cref="ArgumentNullException">
         /// userId
         /// userId
         /// or
         /// or
-        /// key
+        /// key.
         /// </exception>
         /// </exception>
-        public UserItemData GetUserData(long internalUserId, string key)
+        public UserItemData GetUserData(long userId, string key)
         {
         {
-            if (internalUserId <= 0)
+            if (userId <= 0)
             {
             {
-                throw new ArgumentNullException(nameof(internalUserId));
+                throw new ArgumentNullException(nameof(userId));
             }
             }
 
 
             if (string.IsNullOrEmpty(key))
             if (string.IsNullOrEmpty(key))
@@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
                 using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
                 {
                 {
-                    statement.TryBind("@UserId", internalUserId);
+                    statement.TryBind("@UserId", userId);
                     statement.TryBind("@Key", key);
                     statement.TryBind("@Key", key);
 
 
                     foreach (var row in statement.ExecuteQuery())
                     foreach (var row in statement.ExecuteQuery())
@@ -301,7 +302,7 @@ namespace Emby.Server.Implementations.Data
             }
             }
         }
         }
 
 
-        public UserItemData GetUserData(long internalUserId, List<string> keys)
+        public UserItemData GetUserData(long userId, List<string> keys)
         {
         {
             if (keys == null)
             if (keys == null)
             {
             {
@@ -313,19 +314,19 @@ namespace Emby.Server.Implementations.Data
                 return null;
                 return null;
             }
             }
 
 
-            return GetUserData(internalUserId, keys[0]);
+            return GetUserData(userId, keys[0]);
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Return all user-data associated with the given user.
         /// Return all user-data associated with the given user.
         /// </summary>
         /// </summary>
-        /// <param name="internalUserId"></param>
-        /// <returns></returns>
-        public List<UserItemData> GetAllUserData(long internalUserId)
+        /// <param name="userId">The internal user id.</param>
+        /// <returns>The list of user item data.</returns>
+        public List<UserItemData> GetAllUserData(long userId)
         {
         {
-            if (internalUserId <= 0)
+            if (userId <= 0)
             {
             {
-                throw new ArgumentNullException(nameof(internalUserId));
+                throw new ArgumentNullException(nameof(userId));
             }
             }
 
 
             var list = new List<UserItemData>();
             var list = new List<UserItemData>();
@@ -334,7 +335,7 @@ namespace Emby.Server.Implementations.Data
             {
             {
                 using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
                 using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
                 {
                 {
-                    statement.TryBind("@UserId", internalUserId);
+                    statement.TryBind("@UserId", userId);
 
 
                     foreach (var row in statement.ExecuteQuery())
                     foreach (var row in statement.ExecuteQuery())
                     {
                     {
@@ -349,7 +350,8 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// <summary>
         /// Read a row from the specified reader into the provided userData object.
         /// Read a row from the specified reader into the provided userData object.
         /// </summary>
         /// </summary>
-        /// <param name="reader"></param>
+        /// <param name="reader">The list of result set values.</param>
+        /// <returns>The user item data.</returns>
         private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
         private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
         {
         {
             var userData = new UserItemData();
             var userData = new UserItemData();

+ 30 - 0
Emby.Server.Implementations/Data/SynchronouseMode.cs

@@ -0,0 +1,30 @@
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// The disk synchronization mode, controls how aggressively SQLite will write data
+/// all the way out to physical storage.
+/// </summary>
+public enum SynchronousMode
+{
+    /// <summary>
+    /// SQLite continues without syncing as soon as it has handed data off to the operating system.
+    /// </summary>
+    Off = 0,
+
+    /// <summary>
+    /// SQLite database engine will still sync at the most critical moments.
+    /// </summary>
+    Normal = 1,
+
+    /// <summary>
+    /// SQLite database engine will use the xSync method of the VFS
+    /// to ensure that all content is safely written to the disk surface prior to continuing.
+    /// </summary>
+    Full = 2,
+
+    /// <summary>
+    /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
+    /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
+    /// </summary>
+    Extra = 3
+}

+ 23 - 0
Emby.Server.Implementations/Data/TempStoreMode.cs

@@ -0,0 +1,23 @@
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// Storage mode used by temporary database files.
+/// </summary>
+public enum TempStoreMode
+{
+    /// <summary>
+    /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
+    /// is used to determine where temporary tables and indices are stored.
+    /// </summary>
+    Default = 0,
+
+    /// <summary>
+    /// Temporary tables and indices are stored in a file.
+    /// </summary>
+    File = 1,
+
+    /// <summary>
+    /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
+    /// </summary>
+    Memory = 2
+}

+ 10 - 11
Emby.Server.Implementations/Devices/DeviceId.cs

@@ -15,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
     {
     {
         private readonly IApplicationPaths _appPaths;
         private readonly IApplicationPaths _appPaths;
         private readonly ILogger<DeviceId> _logger;
         private readonly ILogger<DeviceId> _logger;
-
         private readonly object _syncLock = new object();
         private readonly object _syncLock = new object();
 
 
+        private string _id;
+
+        public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
+        {
+            _appPaths = appPaths;
+            _logger = loggerFactory.CreateLogger<DeviceId>();
+        }
+
+        public string Value => _id ?? (_id = GetDeviceId());
+
         private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
         private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
 
 
         private string GetCachedId()
         private string GetCachedId()
@@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
 
 
             return id;
             return id;
         }
         }
-
-        private string _id;
-
-        public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
-        {
-            _appPaths = appPaths;
-            _logger = loggerFactory.CreateLogger<DeviceId>();
-        }
-
-        public string Value => _id ?? (_id = GetDeviceId());
     }
     }
 }
 }

+ 0 - 146
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -1,146 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Devices
-{
-    public class DeviceManager : IDeviceManager
-    {
-        private readonly IUserManager _userManager;
-        private readonly IAuthenticationRepository _authRepo;
-        private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
-
-        public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
-        {
-            _userManager = userManager;
-            _authRepo = authRepo;
-        }
-
-        public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
-
-        public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
-        {
-            _capabilitiesMap[deviceId] = capabilities;
-        }
-
-        public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
-        {
-            _authRepo.UpdateDeviceOptions(deviceId, options);
-
-            DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
-        }
-
-        public DeviceOptions GetDeviceOptions(string deviceId)
-        {
-            return _authRepo.GetDeviceOptions(deviceId);
-        }
-
-        public ClientCapabilities GetCapabilities(string id)
-        {
-            return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
-                ? result
-                : new ClientCapabilities();
-        }
-
-        public DeviceInfo GetDevice(string id)
-        {
-            var session = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                DeviceId = id
-            }).Items.FirstOrDefault();
-
-            var device = session == null ? null : ToDeviceInfo(session);
-
-            return device;
-        }
-
-        public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
-        {
-            IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                // UserId = query.UserId
-                HasUser = true
-            }).Items;
-
-            // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
-            if (query.SupportsSync.HasValue)
-            {
-                var val = query.SupportsSync.Value;
-
-                sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
-            }
-
-            if (!query.UserId.Equals(Guid.Empty))
-            {
-                var user = _userManager.GetUserById(query.UserId);
-
-                sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
-            }
-
-            var array = sessions.Select(ToDeviceInfo).ToArray();
-
-            return new QueryResult<DeviceInfo>(array);
-        }
-
-        private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
-        {
-            var caps = GetCapabilities(authInfo.DeviceId);
-
-            return new DeviceInfo
-            {
-                AppName = authInfo.AppName,
-                AppVersion = authInfo.AppVersion,
-                Id = authInfo.DeviceId,
-                LastUserId = authInfo.UserId,
-                LastUserName = authInfo.UserName,
-                Name = authInfo.DeviceName,
-                DateLastActivity = authInfo.DateLastActivity,
-                IconUrl = caps?.IconUrl
-            };
-        }
-
-        public bool CanAccessDevice(User user, string deviceId)
-        {
-            if (user == null)
-            {
-                throw new ArgumentException("user not found");
-            }
-
-            if (string.IsNullOrEmpty(deviceId))
-            {
-                throw new ArgumentNullException(nameof(deviceId));
-            }
-
-            if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
-            {
-                return true;
-            }
-
-            if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
-            {
-                var capabilities = GetCapabilities(deviceId);
-
-                if (capabilities != null && capabilities.SupportsPersistentIdentifier)
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-    }
-}

+ 46 - 72
Emby.Server.Implementations/Dto/DtoService.cs

@@ -7,9 +7,9 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
@@ -51,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 
 
-        private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
-
         public DtoService(
         public DtoService(
             ILogger<DtoService> logger,
             ILogger<DtoService> logger,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
@@ -75,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
             _livetvManagerFactory = livetvManagerFactory;
             _livetvManagerFactory = livetvManagerFactory;
         }
         }
 
 
+        private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         {
         {
@@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto
             var dto = GetBaseItemDtoInternal(item, options, user, owner);
             var dto = GetBaseItemDtoInternal(item, options, user, owner);
             if (item is LiveTvChannel tvChannel)
             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)
             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
             if (item is IItemByName itemByName
@@ -297,7 +294,7 @@ namespace Emby.Server.Implementations.Dto
                             path = path.TrimStart('.');
                             path = path.TrimStart('.');
                         }
                         }
 
 
-                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
+                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
                         {
                         {
                             fileExtensionContainer = path;
                             fileExtensionContainer = path;
                         }
                         }
@@ -373,6 +370,12 @@ namespace Emby.Server.Implementations.Dto
                     if (item is MusicAlbum || item is Season || item is Playlist)
                     if (item is MusicAlbum || item is Season || item is Playlist)
                     {
                     {
                         dto.ChildCount = dto.RecursiveItemCount;
                         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))
                     if (options.ContainsField(ItemFields.ChildCount))
@@ -420,7 +423,7 @@ namespace Emby.Server.Implementations.Dto
             // Just return something so that apps that are expecting a value won't think the folders are empty
             // Just return something so that apps that are expecting a value won't think the folders are empty
             if (folder is ICollectionFolder || folder is UserView)
             if (folder is ICollectionFolder || folder is UserView)
             {
             {
-                return new Random().Next(1, 10);
+                return Random.Shared.Next(1, 10);
             }
             }
 
 
             return folder.GetChildCount(user);
             return folder.GetChildCount(user);
@@ -467,7 +470,7 @@ namespace Emby.Server.Implementations.Dto
             {
             {
                 var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
                 var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
                 {
                 {
-                    IncludeItemTypes = new[] { nameof(MusicAlbum) },
+                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
                     Name = item.Album,
                     Name = item.Album,
                     Limit = 1
                     Limit = 1
                 });
                 });
@@ -497,7 +500,7 @@ namespace Emby.Server.Implementations.Dto
             }
             }
             catch (Exception ex)
             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;
                 return null;
             }
             }
         }
         }
@@ -507,7 +510,6 @@ namespace Emby.Server.Implementations.Dto
         /// </summary>
         /// </summary>
         /// <param name="dto">The dto.</param>
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <returns>Task.</returns>
         private void AttachPeople(BaseItemDto dto, BaseItem item)
         private void AttachPeople(BaseItemDto dto, BaseItem item)
         {
         {
             // Ordering by person type to ensure actors and artists are at the front.
             // Ordering by person type to ensure actors and artists are at the front.
@@ -616,7 +618,6 @@ namespace Emby.Server.Implementations.Dto
         /// </summary>
         /// </summary>
         /// <param name="dto">The dto.</param>
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <returns>Task.</returns>
         private void AttachStudios(BaseItemDto dto, BaseItem item)
         private void AttachStudios(BaseItemDto dto, BaseItem item)
         {
         {
             dto.Studios = item.Studios
             dto.Studios = item.Studios
@@ -757,15 +758,6 @@ namespace Emby.Server.Implementations.Dto
                 dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
                 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))
             if (options.ContainsField(ItemFields.Genres))
             {
             {
                 dto.Genres = item.Genres;
                 dto.Genres = item.Genres;
@@ -807,7 +799,7 @@ namespace Emby.Server.Implementations.Dto
 
 
             dto.MediaType = item.MediaType;
             dto.MediaType = item.MediaType;
 
 
-            if (!(item is LiveTvProgram))
+            if (item is not LiveTvProgram)
             {
             {
                 dto.LocationType = item.LocationType;
                 dto.LocationType = item.LocationType;
             }
             }
@@ -928,9 +920,9 @@ namespace Emby.Server.Implementations.Dto
                 }
                 }
 
 
                 // if (options.ContainsField(ItemFields.MediaSourceCount))
                 // if (options.ContainsField(ItemFields.MediaSourceCount))
-                //{
+                // {
                 // Songs always have one
                 // Songs always have one
-                //}
+                // }
             }
             }
 
 
             if (item is IHasArtist hasArtist)
             if (item is IHasArtist hasArtist)
@@ -938,10 +930,10 @@ namespace Emby.Server.Implementations.Dto
                 dto.Artists = hasArtist.Artists;
                 dto.Artists = hasArtist.Artists;
 
 
                 // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
                 // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
-                //{
+                // {
                 //    EnableTotalRecordCount = false,
                 //    EnableTotalRecordCount = false,
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
-                //});
+                // });
 
 
                 // dto.ArtistItems = artistItems.Items
                 // dto.ArtistItems = artistItems.Items
                 //    .Select(i =>
                 //    .Select(i =>
@@ -958,7 +950,7 @@ namespace Emby.Server.Implementations.Dto
                 // Include artists that are not in the database yet, e.g., just added via metadata editor
                 // Include artists that are not in the database yet, e.g., just added via metadata editor
                 // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
                 // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
                 dto.ArtistItems = hasArtist.Artists
                 dto.ArtistItems = hasArtist.Artists
-                    //.Except(foundArtists, new DistinctNameComparer())
+                    // .Except(foundArtists, new DistinctNameComparer())
                     .Select(i =>
                     .Select(i =>
                     {
                     {
                         // This should not be necessary but we're seeing some cases of it
                         // This should not be necessary but we're seeing some cases of it
@@ -990,10 +982,10 @@ namespace Emby.Server.Implementations.Dto
                 dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
                 dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
 
 
                 // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
                 // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
-                //{
+                // {
                 //    EnableTotalRecordCount = false,
                 //    EnableTotalRecordCount = false,
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
                 //    ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
-                //});
+                // });
 
 
                 // dto.AlbumArtists = artistItems.Items
                 // dto.AlbumArtists = artistItems.Items
                 //    .Select(i =>
                 //    .Select(i =>
@@ -1008,7 +1000,7 @@ namespace Emby.Server.Implementations.Dto
                 //    .ToList();
                 //    .ToList();
 
 
                 dto.AlbumArtists = hasAlbumArtist.AlbumArtists
                 dto.AlbumArtists = hasAlbumArtist.AlbumArtists
-                    //.Except(foundArtists, new DistinctNameComparer())
+                    // .Except(foundArtists, new DistinctNameComparer())
                     .Select(i =>
                     .Select(i =>
                     {
                     {
                         // This should not be necessary but we're seeing some cases of it
                         // This should not be necessary but we're seeing some cases of it
@@ -1035,8 +1027,7 @@ namespace Emby.Server.Implementations.Dto
             }
             }
 
 
             // Add video info
             // Add video info
-            var video = item as Video;
-            if (video != null)
+            if (item is Video video)
             {
             {
                 dto.VideoType = video.VideoType;
                 dto.VideoType = video.VideoType;
                 dto.Video3DFormat = video.Video3DFormat;
                 dto.Video3DFormat = video.Video3DFormat;
@@ -1075,9 +1066,7 @@ namespace Emby.Server.Implementations.Dto
             if (options.ContainsField(ItemFields.MediaStreams))
             if (options.ContainsField(ItemFields.MediaStreams))
             {
             {
                 // Add VideoInfo
                 // Add VideoInfo
-                var iHasMediaSources = item as IHasMediaSources;
-
-                if (iHasMediaSources != null)
+                if (item is IHasMediaSources)
                 {
                 {
                     MediaStream[] mediaStreams;
                     MediaStream[] mediaStreams;
 
 
@@ -1146,7 +1135,7 @@ namespace Emby.Server.Implementations.Dto
                 // TODO maybe remove the if statement entirely
                 // TODO maybe remove the if statement entirely
                 // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 {
                 {
-                    episodeSeries = episodeSeries ?? episode.Series;
+                    episodeSeries ??= episode.Series;
                     if (episodeSeries != null)
                     if (episodeSeries != null)
                     {
                     {
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
@@ -1159,7 +1148,7 @@ namespace Emby.Server.Implementations.Dto
 
 
                 if (options.ContainsField(ItemFields.SeriesStudio))
                 if (options.ContainsField(ItemFields.SeriesStudio))
                 {
                 {
-                    episodeSeries = episodeSeries ?? episode.Series;
+                    episodeSeries ??= episode.Series;
                     if (episodeSeries != null)
                     if (episodeSeries != null)
                     {
                     {
                         dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
                         dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
@@ -1172,7 +1161,7 @@ namespace Emby.Server.Implementations.Dto
             {
             {
                 dto.AirDays = series.AirDays;
                 dto.AirDays = series.AirDays;
                 dto.AirTime = series.AirTime;
                 dto.AirTime = series.AirTime;
-                dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
+                dto.Status = series.Status?.ToString();
             }
             }
 
 
             // Add SeasonInfo
             // Add SeasonInfo
@@ -1185,7 +1174,7 @@ namespace Emby.Server.Implementations.Dto
 
 
                 if (options.ContainsField(ItemFields.SeriesStudio))
                 if (options.ContainsField(ItemFields.SeriesStudio))
                 {
                 {
-                    series = series ?? season.Series;
+                    series ??= season.Series;
                     if (series != null)
                     if (series != null)
                     {
                     {
                         dto.SeriesStudio = series.Studios.FirstOrDefault();
                         dto.SeriesStudio = series.Studios.FirstOrDefault();
@@ -1196,7 +1185,7 @@ namespace Emby.Server.Implementations.Dto
                 // TODO maybe remove the if statement entirely
                 // TODO maybe remove the if statement entirely
                 // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
                 {
                 {
-                    series = series ?? season.Series;
+                    series ??= season.Series;
                     if (series != null)
                     if (series != null)
                     {
                     {
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
                         dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
@@ -1283,7 +1272,7 @@ namespace Emby.Server.Implementations.Dto
 
 
             var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
             var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
 
 
-            if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
+            if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
             {
             {
                 parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
                 parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
             }
             }
@@ -1316,9 +1305,12 @@ namespace Emby.Server.Implementations.Dto
 
 
             var imageTags = dto.ImageTags;
             var imageTags = dto.ImageTags;
 
 
-            while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
-                (parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
+            while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
+                || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
+                || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
+                || parent is Series)
             {
             {
+                parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
                 if (parent == null)
                 if (parent == null)
                 {
                 {
                     break;
                     break;
@@ -1348,7 +1340,7 @@ namespace Emby.Server.Implementations.Dto
                     }
                     }
                 }
                 }
 
 
-                if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
+                if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
                 {
                 {
                     var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
                     var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
 
 
@@ -1398,7 +1390,6 @@ namespace Emby.Server.Implementations.Dto
         /// </summary>
         /// </summary>
         /// <param name="dto">The dto.</param>
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <returns>Task.</returns>
         public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
         public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
         {
         {
             dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
             dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
@@ -1413,44 +1404,27 @@ namespace Emby.Server.Implementations.Dto
                 return null;
                 return null;
             }
             }
 
 
-            ImageDimensions size;
-
-            var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
-
-            if (defaultAspectRatio > 0)
-            {
-                return defaultAspectRatio;
-            }
-
             if (!imageInfo.IsLocalFile)
             if (!imageInfo.IsLocalFile)
             {
             {
-                return null;
+                return item.GetDefaultPrimaryImageAspectRatio();
             }
             }
 
 
             try
             try
             {
             {
-                size = _imageProcessor.GetImageDimensions(item, imageInfo);
-
-                if (size.Width <= 0 || size.Height <= 0)
+                var size = _imageProcessor.GetImageDimensions(item, imageInfo);
+                var width = size.Width;
+                var height = size.Height;
+                if (width > 0 && height > 0)
                 {
                 {
-                    return null;
+                    return (double)width / height;
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
-                _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
-                return null;
-            }
-
-            var width = size.Width;
-            var height = size.Height;
-
-            if (width <= 0 || height <= 0)
-            {
-                return null;
+                _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
             }
             }
 
 
-            return (double)width / height;
+            return item.GetDefaultPrimaryImageAspectRatio();
         }
         }
     }
     }
 }
 }

+ 11 - 14
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -23,17 +23,18 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
-    <PackageReference Include="Mono.Nat" Version="3.0.1" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
-    <PackageReference Include="sharpcompress" Version="0.28.3" />
+    <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.1" />
+    <PackageReference Include="Mono.Nat" Version="3.0.2" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
+    <PackageReference Include="sharpcompress" Version="0.30.1" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
-    <PackageReference Include="DotNet.Glob" Version="3.1.2" />
+    <PackageReference Include="DotNet.Glob" Version="3.1.3" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -41,15 +42,11 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
-    <Nullable>enable</Nullable>
     <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
     <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
     <NoWarn>AD0001</NoWarn>
     <NoWarn>AD0001</NoWarn>
-    <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->

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

@@ -9,12 +9,10 @@ using System.Net;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using Jellyfin.Data.Events;
 using Jellyfin.Networking.Configuration;
 using Jellyfin.Networking.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Dlna;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Mono.Nat;
 using Mono.Nat;
 
 
@@ -28,7 +26,6 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IServerApplicationHost _appHost;
         private readonly IServerApplicationHost _appHost;
         private readonly ILogger<ExternalPortForwarding> _logger;
         private readonly ILogger<ExternalPortForwarding> _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
-        private readonly IDeviceDiscovery _deviceDiscovery;
 
 
         private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
         private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
 
 
@@ -43,17 +40,14 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="config">The configuration manager.</param>
         /// <param name="config">The configuration manager.</param>
-        /// <param name="deviceDiscovery">The device discovery.</param>
         public ExternalPortForwarding(
         public ExternalPortForwarding(
             ILogger<ExternalPortForwarding> logger,
             ILogger<ExternalPortForwarding> logger,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
-            IServerConfigurationManager config,
-            IDeviceDiscovery deviceDiscovery)
+            IServerConfigurationManager config)
         {
         {
             _logger = logger;
             _logger = logger;
             _appHost = appHost;
             _appHost = appHost;
             _config = config;
             _config = config;
-            _deviceDiscovery = deviceDiscovery;
         }
         }
 
 
         private string GetConfigIdentifier()
         private string GetConfigIdentifier()

+ 3 - 3
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
         private static bool EnableRefreshMessage(BaseItem item)
         private static bool EnableRefreshMessage(BaseItem item)
         {
         {
-            if (!(item is Folder folder))
+            if (item is not Folder folder)
             {
             {
                 return false;
                 return false;
             }
             }
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
                 return false;
                 return false;
             }
             }
 
 
-            if (item is IItemByName && !(item is MusicArtist))
+            if (item is IItemByName && item is not MusicArtist)
             {
             {
                 return false;
                 return false;
             }
             }
@@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <summary>
         /// <summary>
         /// Translates the physical item to user library.
         /// Translates the physical item to user library.
         /// </summary>
         /// </summary>
-        /// <typeparam name="T"></typeparam>
+        /// <typeparam name="T">The type of item.</typeparam>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="user">The user.</param>
         /// <param name="user">The user.</param>
         /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
         /// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>

+ 3 - 0
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -37,6 +37,9 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
         /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
         /// </summary>
         /// </summary>
+        /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
+        /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         public UdpServerEntryPoint(
         public UdpServerEntryPoint(
             ILogger<UdpServerEntryPoint> logger,
             ILogger<UdpServerEntryPoint> logger,
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,

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

@@ -1,7 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
+using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 
 
@@ -17,13 +17,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _authorizationContext = authorizationContext;
             _authorizationContext = authorizationContext;
         }
         }
 
 
-        public AuthorizationInfo Authenticate(HttpRequest request)
+        public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
         {
         {
-            var auth = _authorizationContext.GetAuthorizationInfo(request);
+            var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
 
 
             if (!auth.HasToken)
             if (!auth.HasToken)
             {
             {
-                throw new AuthenticationException("Request does not contain a token.");
+                return auth;
             }
             }
 
 
             if (!auth.IsAuthenticated)
             if (!auth.IsAuthenticated)

+ 14 - 7
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
         }
         }
 
 
-        public SessionInfo GetSession(HttpContext requestContext)
+        public async Task<SessionInfo> GetSession(HttpContext requestContext)
         {
         {
-            var authorization = _authContext.GetAuthorizationInfo(requestContext);
+            var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
 
 
             var user = authorization.User;
             var user = authorization.User;
-            return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
+            return await _sessionManager.LogSessionActivity(
+                authorization.Client,
+                authorization.Version,
+                authorization.DeviceId,
+                authorization.Device,
+                requestContext.GetNormalizedRemoteIp().ToString(),
+                user).ConfigureAwait(false);
         }
         }
 
 
-        public SessionInfo GetSession(object requestContext)
+        public Task<SessionInfo> GetSession(object requestContext)
         {
         {
             return GetSession((HttpContext)requestContext);
             return GetSession((HttpContext)requestContext);
         }
         }
 
 
-        public User? GetUser(HttpContext requestContext)
+        public async Task<User?> GetUser(HttpContext requestContext)
         {
         {
-            var session = GetSession(requestContext);
+            var session = await GetSession(requestContext).ConfigureAwait(false);
 
 
             return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
             return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
         }
         }
 
 
-        public User? GetUser(object requestContext)
+        public Task<User?> GetUser(object requestContext)
         {
         {
             return GetUser(((HttpRequest)requestContext).HttpContext);
             return GetUser(((HttpRequest)requestContext).HttpContext);
         }
         }

+ 6 - 6
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -7,7 +7,7 @@ using System.Text;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
         public event EventHandler<EventArgs>? Closed;
         public event EventHandler<EventArgs>? Closed;
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the remote end point.
+        /// Gets the remote end point.
         /// </summary>
         /// </summary>
         public IPAddress? RemoteEndPoint { get; }
         public IPAddress? RemoteEndPoint { get; }
 
 
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer
         public DateTime LastKeepAliveDate { get; set; }
         public DateTime LastKeepAliveDate { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the query string.
+        /// Gets the query string.
         /// </summary>
         /// </summary>
         /// <value>The query string.</value>
         /// <value>The query string.</value>
         public IQueryCollection QueryString { get; }
         public IQueryCollection QueryString { get; }
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// <summary>
         /// Sends a message asynchronously.
         /// Sends a message asynchronously.
         /// </summary>
         /// </summary>
-        /// <typeparam name="T"></typeparam>
+        /// <typeparam name="T">The type of the message.</typeparam>
         /// <param name="message">The message.</param>
         /// <param name="message">The message.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
@@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
                 {
                 {
                     await ProcessInternal(pipe.Reader).ConfigureAwait(false);
                     await ProcessInternal(pipe.Reader).ConfigureAwait(false);
                 }
                 }
-            } while (
-                (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+            }
+            while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
                 && receiveresult.MessageType != WebSocketMessageType.Close);
                 && receiveresult.MessageType != WebSocketMessageType.Close);
 
 
             Closed?.Invoke(this, EventArgs.Empty);
             Closed?.Invoke(this, EventArgs.Empty);

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

@@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer
         /// <inheritdoc />
         /// <inheritdoc />
         public async Task WebSocketRequestHandler(HttpContext context)
         public async Task WebSocketRequestHandler(HttpContext context)
         {
         {
-            _ = _authService.Authenticate(context.Request);
+            var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
+            if (!authorizationInfo.IsAuthenticated)
+            {
+                throw new SecurityException("Token is required");
+            }
+
             try
             try
             {
             {
                 _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
                 _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
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.IO
 namespace Emby.Server.Implementations.IO
 {
 {
-    public class FileRefresher : IDisposable
+    public sealed class FileRefresher : IDisposable
     {
     {
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
 
 
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly object _timerLock = new object();
         private readonly object _timerLock = new object();
-        private Timer _timer;
+        private Timer? _timer;
         private bool _disposed;
         private bool _disposed;
 
 
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
             AddPath(path);
             AddPath(path);
         }
         }
 
 
-        public event EventHandler<EventArgs> Completed;
+        public event EventHandler<EventArgs>? Completed;
 
 
         public string Path { get; private set; }
         public string Path { get; private set; }
 
 
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
             RestartTimer();
             RestartTimer();
         }
         }
 
 
-        private void OnTimerCallback(object state)
+        private void OnTimerCallback(object? state)
         {
         {
             List<string> paths;
             List<string> paths;
 
 
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
 
 
             try
             try
             {
             {
-                ProcessPathChanges(paths.ToList());
+                ProcessPathChanges(paths);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
 
 
         private void ProcessPathChanges(List<string> paths)
         private void ProcessPathChanges(List<string> paths)
         {
         {
-            var itemsToRefresh = paths
+            IEnumerable<BaseItem> itemsToRefresh = paths
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(GetAffectedBaseItem)
                 .Select(GetAffectedBaseItem)
                 .Where(item => item != null)
                 .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)
             foreach (var item in itemsToRefresh)
             {
             {
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         /// <returns>BaseItem.</returns>
         /// <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))
             while (item == null && !string.IsNullOrEmpty(path))
             {
             {
                 item = _libraryManager.FindByPath(path, null);
                 item = _libraryManager.FindByPath(path, null);
 
 
-                path = System.IO.Path.GetDirectoryName(path);
+                path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
             }
             }
 
 
             if (item != null)
             if (item != null)

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