浏览代码

Merge remote-tracking branch 'upstream/master' into healthy-base-url

crobibero 4 年之前
父节点
当前提交
01e8ff8ddf
共有 100 个文件被更改,包括 1097 次插入752 次删除
  1. 0 59
      .ci/azure-pipelines-api-client.yml
  2. 0 3
      .ci/azure-pipelines.yml
  3. 7 1
      .github/ISSUE_TEMPLATE/bug_report.md
  4. 7 1
      .github/dependabot.yml
  5. 43 0
      .github/label-commenter-config.yml
  6. 64 0
      .github/workflows/automation.yml
  7. 22 0
      .github/workflows/label-commenter.yml
  8. 17 0
      .github/workflows/merge-conflicts.yml
  9. 27 0
      .github/workflows/rebase.yml
  10. 1 0
      CONTRIBUTORS.md
  11. 3 3
      Dockerfile
  12. 3 3
      Dockerfile.arm
  13. 3 3
      Dockerfile.arm64
  14. 2 0
      Emby.Dlna/Configuration/DlnaOptions.cs
  15. 0 1
      Emby.Dlna/ConfigurationExtension.cs
  16. 2 0
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  17. 2 2
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  18. 2 0
      Emby.Dlna/ControlRequest.cs
  19. 2 0
      Emby.Dlna/ControlResponse.cs
  20. 26 11
      Emby.Dlna/Didl/DidlBuilder.cs
  21. 1 1
      Emby.Dlna/Didl/StringWriterWithEncoding.cs
  22. 0 1
      Emby.Dlna/DlnaConfigurationFactory.cs
  23. 32 65
      Emby.Dlna/DlnaManager.cs
  24. 1 0
      Emby.Dlna/Emby.Dlna.csproj
  25. 2 0
      Emby.Dlna/EventSubscriptionResponse.cs
  26. 2 0
      Emby.Dlna/Eventing/DlnaEventManager.cs
  27. 2 0
      Emby.Dlna/Eventing/EventSubscription.cs
  28. 2 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  29. 0 1
      Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
  30. 38 0
      Emby.Dlna/PlayTo/Device.cs
  31. 2 0
      Emby.Dlna/PlayTo/DeviceInfo.cs
  32. 2 0
      Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
  33. 47 4
      Emby.Dlna/PlayTo/PlayToController.cs
  34. 3 1
      Emby.Dlna/PlayTo/PlayToManager.cs
  35. 2 0
      Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
  36. 2 0
      Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
  37. 2 0
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  38. 2 0
      Emby.Dlna/PlayTo/PlaylistItem.cs
  39. 2 0
      Emby.Dlna/PlayTo/PlaylistItemFactory.cs
  40. 2 1
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  41. 7 7
      Emby.Dlna/PlayTo/TransportCommands.cs
  42. 2 0
      Emby.Dlna/PlayTo/uBaseObject.cs
  43. 2 1
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  44. 2 2
      Emby.Dlna/Service/BaseControlHandler.cs
  45. 3 1
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  46. 3 3
      Emby.Dlna/Ssdp/SsdpExtensions.cs
  47. 3 3
      Emby.Naming/Audio/AudioFileParser.cs
  48. 3 2
      Emby.Naming/Emby.Naming.csproj
  49. 5 0
      Emby.Naming/TV/EpisodeResolver.cs
  50. 49 46
      Emby.Naming/Video/ExtraResolver.cs
  51. 10 12
      Emby.Naming/Video/VideoResolver.cs
  52. 2 6
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  53. 2 0
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  54. 0 3
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  55. 5 15
      Emby.Server.Implementations/ApplicationHost.cs
  56. 2 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  57. 2 2
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  58. 36 34
      Emby.Server.Implementations/Collections/CollectionManager.cs
  59. 2 0
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  60. 0 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  61. 0 2
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  62. 4 4
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  63. 3 3
      Emby.Server.Implementations/Data/ManagedConnection.cs
  64. 108 17
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  65. 238 286
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  66. 11 9
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  67. 3 3
      Emby.Server.Implementations/Data/TypeMapper.cs
  68. 2 0
      Emby.Server.Implementations/Devices/DeviceId.cs
  69. 2 0
      Emby.Server.Implementations/Devices/DeviceManager.cs
  70. 5 12
      Emby.Server.Implementations/Dto/DtoService.cs
  71. 6 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  72. 2 0
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  73. 2 0
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  74. 2 0
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  75. 0 2
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  76. 2 0
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  77. 0 1
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  78. 27 27
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  79. 2 2
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  80. 0 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  81. 2 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  82. 2 0
      Emby.Server.Implementations/IO/FileRefresher.cs
  83. 2 0
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  84. 34 33
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  85. 1 1
      Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
  86. 1 1
      Emby.Server.Implementations/IO/StreamHelper.cs
  87. 0 2
      Emby.Server.Implementations/IStartupOptions.cs
  88. 0 8
      Emby.Server.Implementations/Images/ArtistImageProvider.cs
  89. 3 1
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  90. 2 0
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  91. 2 1
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  92. 2 0
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  93. 2 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  94. 3 3
      Emby.Server.Implementations/Images/PlaylistImageProvider.cs
  95. 2 0
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  96. 0 2
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  97. 84 11
      Emby.Server.Implementations/Library/LibraryManager.cs
  98. 2 0
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  99. 13 14
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  100. 2 0
      Emby.Server.Implementations/Library/MediaStreamSelector.cs

+ 0 - 59
.ci/azure-pipelines-api-client.yml

@@ -1,59 +0,0 @@
-parameters:
-  - name: LinuxImage
-    type: string
-    default: "ubuntu-latest"
-  - name: GeneratorVersion
-    type: string
-    default: "5.0.1"
-
-jobs:
-- job: GenerateApiClients
-  displayName: 'Generate Api Clients'
-  condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-  dependsOn: Test
-
-  pool:
-    vmImage: "${{ parameters.LinuxImage }}"
-
-  steps:
-    - task: DownloadPipelineArtifact@2
-      displayName: 'Download OpenAPI Spec Artifact'
-      inputs:
-        source: 'current'
-        artifact: "OpenAPI Spec"
-        path: "$(System.ArtifactsDirectory)/openapispec"
-        runVersion: "latest"
-
-    - task: CmdLine@2
-      displayName: 'Download OpenApi Generator'
-      inputs:
-        script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
-
-## Authenticate with npm registry
-    - task: npmAuthenticate@0
-      inputs:
-        workingFile: ./.npmrc
-        customEndpoint: 'jellyfin-bot for NPM'
-
-## Generate npm api client
-    - task: CmdLine@2
-      displayName: 'Build stable typescript axios client'
-      inputs:
-        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
-
-## Run npm install
-    - task: Npm@1
-      displayName: 'Install npm dependencies'
-      inputs:
-        command: install
-        workingDir: ./apiclient/generated/typescript/axios
-
-## Publish npm packages
-    - task: Npm@1
-      displayName: 'Publish stable typescript axios client'
-      inputs:
-        command: custom
-        customCommand: publish --access public
-        publishRegistry: useExternalRegistry
-        publishEndpoint: 'jellyfin-bot for NPM'
-        workingDir: ./apiclient/generated/typescript/axios

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

@@ -61,6 +61,3 @@ jobs:
 
 - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
   - template: azure-pipelines-package.yml
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
-  - template: azure-pipelines-api-client.yml

+ 7 - 1
.github/ISSUE_TEMPLATE/bug_report.md

@@ -33,7 +33,13 @@ assignees: ''
 **Expected behavior**
 <!-- A clear and concise description of what you expected to happen. -->
 
-**Logs**
+**Server Logs**
+<!-- Please paste any log errors. -->
+
+**FFmpeg Logs**
+<!-- Please paste any log errors. -->
+
+**Browser Console Logs**
 <!-- Please paste any log errors. -->
 
 **Screenshots**

+ 7 - 1
.github/dependabot.yml

@@ -6,4 +6,10 @@ updates:
     interval: weekly
     time: '12:00'
   open-pull-requests-limit: 10
-  
+
+- package-ecosystem: github-actions
+  directory: '/'
+  schedule:
+    interval: weekly
+    time: '12:00'
+  open-pull-requests-limit: 10

+ 43 - 0
.github/label-commenter-config.yml

@@ -0,0 +1,43 @@
+comment:
+  header: Hello @{{ issue.user.login }}
+  footer: "\
+    ---\n\n
+    > This is an automated comment created by the [peaceiris/actions-label-commenter]. \
+    Responding to the bot or mentioning it won't have any effect.\n\n
+    [peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter
+    "
+
+labels:
+  - name: stable backport
+    labeled:
+      pr:
+        body: |
+          This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release.
+
+          Please observe the following:
+
+            * Any dependent PRs that this PR requires **must** be tagged for stable backporting as well.
+
+            * Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release.
+          
+            * This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided.
+              
+              To do this, run the following commands from your local copy of the Jellyfin repository:
+              
+                1. `git checkout master`
+
+                1. `git merge --no-ff <myPullRequestBranch>`
+
+                1. `git log` -> `commit xxxxxxxxx`, grab hash
+
+                1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`)
+
+                1. `git cherry-pick -sx -m1 <hash>`
+
+              Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff.
+
+              Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly.
+
+              **Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state.
+
+              Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable.

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

@@ -0,0 +1,64 @@
+name: Automation
+
+on:
+  pull_request:
+
+jobs:
+  main:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Does PR has the stable backport label?
+        uses: Dreamcodeio/does-pr-has-label@v1.2
+        id: checkLabel
+        with:
+          label: stable backport
+
+      - name: Remove from 'Current Release' project
+        uses: alex-page/github-project-automation-plus@v0.7.1
+        if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
+        continue-on-error: true
+        with:
+          project: Current Release
+          action: delete
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+      - name: Add to 'Release Next' project
+        uses: alex-page/github-project-automation-plus@v0.7.1
+        if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
+        continue-on-error: true
+        with:
+          project: Release Next
+          column: In progress
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+      - name: Add to 'Current Release' project
+        uses: alex-page/github-project-automation-plus@v0.7.1
+        if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
+        continue-on-error: true
+        with:
+          project: Current Release
+          column: In progress
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+      - name: Check number of comments from the team member
+        if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
+        id: member_comments
+        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
+        uses: alex-page/github-project-automation-plus@v0.7.1
+        if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
+        continue-on-error: true
+        with:
+          project: Issue Triage for Main Repo
+          column: Needs triage
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
+
+      - name: Add issue to triage project
+        uses: alex-page/github-project-automation-plus@v0.7.1
+        if: github.event.issue.pull_request == '' && github.event.action == 'opened'
+        continue-on-error: true
+        with:
+          project: Issue Triage for Main Repo
+          column: Pending response
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}

+ 22 - 0
.github/workflows/label-commenter.yml

@@ -0,0 +1,22 @@
+name: Label Commenter
+
+on:
+  issues:
+    types:
+      - labeled
+      - unlabeled
+  pull_request_target:
+    types:
+      - labeled
+      - unlabeled
+
+jobs:
+  comment:
+    runs-on: ubuntu-20.04
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          ref: master
+
+      - name: Label Commenter
+        uses: peaceiris/actions-label-commenter@v1

+ 17 - 0
.github/workflows/merge-conflicts.yml

@@ -0,0 +1,17 @@
+name: 'Merge Conflicts'
+
+on:
+  push:
+    branches:
+      - master
+  pull_request_target:
+    types:
+      - synchronize
+jobs:
+  triage:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: eps1lon/actions-label-merge-conflict@v2.0.1
+        with:
+          dirtyLabel: 'merge conflict'
+          repoToken: ${{ secrets.JF_BOT_TOKEN }}

+ 27 - 0
.github/workflows/rebase.yml

@@ -0,0 +1,27 @@
+name: Automatic Rebase
+on:
+  issue_comment:
+
+jobs:
+  rebase:
+    name: Rebase
+    if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Notify as seen
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          comment-id: ${{ github.event.comment.id }}
+          reactions: '+1'
+
+      - name: Checkout the latest code
+        uses: actions/checkout@v2
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          fetch-depth: 0
+
+      - name: Automatic Rebase
+        uses: cirrus-actions/rebase@1.4
+        env:
+          GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

+ 1 - 0
CONTRIBUTORS.md

@@ -146,6 +146,7 @@
  - [nielsvanvelzen](https://github.com/nielsvanvelzen)
  - [skyfrk](https://github.com/skyfrk)
  - [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
 
 # Emby Contributors
 

+ 3 - 3
Dockerfile

@@ -1,11 +1,11 @@
 ARG DOTNET_VERSION=5.0
 
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
- && npm ci --no-audit \
+ && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
 FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder

+ 3 - 3
Dockerfile.arm

@@ -5,12 +5,12 @@
 ARG DOTNET_VERSION=5.0
 
 
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
- && npm ci --no-audit \
+ && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
 

+ 3 - 3
Dockerfile.arm64

@@ -5,12 +5,12 @@
 ARG DOTNET_VERSION=5.0
 
 
-FROM node:alpine as web-builder
+FROM node:lts-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
+RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
- && npm ci --no-audit \
+ && npm ci --no-audit --unsafe-perm \
  && mv dist /dist
 
 

+ 2 - 0
Emby.Dlna/Configuration/DlnaOptions.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 namespace Emby.Dlna.Configuration

+ 0 - 1
Emby.Dlna/ConfigurationExtension.cs

@@ -1,4 +1,3 @@
-#nullable enable
 #pragma warning disable CS1591
 
 using Emby.Dlna.Configuration;

+ 2 - 0
Emby.Dlna/ContentDirectory/ContentDirectoryService.cs

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

+ 2 - 2
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -1,5 +1,6 @@
+#nullable disable
+
 using System;
-using System.Collections;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -7,7 +8,6 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Xml;
-using Emby.Dlna.Configuration;
 using Emby.Dlna.Didl;
 using Emby.Dlna.Service;
 using Jellyfin.Data.Entities;

+ 2 - 0
Emby.Dlna/ControlRequest.cs

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

+ 2 - 0
Emby.Dlna/ControlResponse.cs

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

+ 26 - 11
Emby.Dlna/Didl/DidlBuilder.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -208,7 +210,8 @@ namespace Emby.Dlna.Didl
             var targetWidth = streamInfo.TargetWidth;
             var targetHeight = streamInfo.TargetHeight;
 
-            var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
+            var contentFeatureList = ContentFeatureBuilder.BuildVideoHeader(
+                _profile,
                 streamInfo.Container,
                 streamInfo.TargetVideoCodec.FirstOrDefault(),
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
@@ -599,7 +602,8 @@ namespace Emby.Dlna.Didl
                 ? MimeTypes.GetMimeType(filename)
                 : mediaProfile.MimeType;
 
-            var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
+            var contentFeatures = ContentFeatureBuilder.BuildAudioHeader(
+                _profile,
                 streamInfo.Container,
                 streamInfo.TargetAudioCodec.FirstOrDefault(),
                 targetAudioBitrate,
@@ -974,15 +978,28 @@ namespace Emby.Dlna.Didl
                 return;
             }
 
-            var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
+            // TODO: Remove these default values
+            var albumArtUrlInfo = GetImageUrl(
+                imageInfo,
+                _profile.MaxAlbumArtWidth ?? 10000,
+                _profile.MaxAlbumArtHeight ?? 10000,
+                "jpg");
 
             writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
-            writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
-            writer.WriteString(albumartUrlInfo.url);
+            if (!string.IsNullOrEmpty(_profile.AlbumArtPn))
+            {
+                writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
+            }
+
+            writer.WriteString(albumArtUrlInfo.url);
             writer.WriteFullEndElement();
 
-            // TOOD: Remove these default values
-            var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
+            // TODO: Remove these default values
+            var iconUrlInfo = GetImageUrl(
+                imageInfo,
+                _profile.MaxIconWidth ?? 48,
+                _profile.MaxIconHeight ?? 48,
+                "jpg");
             writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
 
             if (!_profile.EnableAlbumArtInDidl)
@@ -1033,8 +1050,7 @@ namespace Emby.Dlna.Didl
             var width = albumartUrlInfo.width ?? maxWidth;
             var height = albumartUrlInfo.height ?? maxHeight;
 
-            var contentFeatures = new ContentFeatureBuilder(_profile)
-                .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
+            var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
 
             writer.WriteAttributeString(
                 "protocolInfo",
@@ -1206,8 +1222,7 @@ namespace Emby.Dlna.Didl
 
             if (width.HasValue && height.HasValue)
             {
-                var newSize = DrawingUtils.Resize(
-                        new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
+                var newSize = DrawingUtils.Resize(new ImageDimensions(width.Value, height.Value), 0, 0, maxWidth, maxHeight);
 
                 width = newSize.Width;
                 height = newSize.Height;

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

@@ -9,7 +9,7 @@ namespace Emby.Dlna.Didl
 {
     public class StringWriterWithEncoding : StringWriter
     {
-        private readonly Encoding _encoding;
+        private readonly Encoding? _encoding;
 
         public StringWriterWithEncoding()
         {

+ 0 - 1
Emby.Dlna/DlnaConfigurationFactory.cs

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

+ 32 - 65
Emby.Dlna/DlnaManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -111,7 +113,7 @@ namespace Emby.Dlna
 
             if (profile != null)
             {
-                _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+                _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
             }
             else
             {
@@ -138,80 +140,45 @@ namespace Emby.Dlna
             _logger.LogInformation(builder.ToString());
         }
 
-        private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
+        /// <summary>
+        /// Attempts to match a device with a profile.
+        /// Rules:
+        /// - If the profile field has no value, the field matches irregardless of its contents.
+        /// - the profile field can be an exact match, or a reg exp.
+        /// </summary>
+        /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
+        /// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
+        /// <returns><b>True</b> if they match.</returns>
+        public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
         {
-            if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
-            {
-                if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
-                {
-                    return false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
-            {
-                if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
-                {
-                    return false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
-            {
-                if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
-                {
-                    return false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
-            {
-                if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
-                {
-                    return false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(profileInfo.ModelName))
-            {
-                if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
-                {
-                    return false;
-                }
-            }
-
-            if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
-            {
-                if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
-                {
-                    return false;
-                }
-            }
+            return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
+                && IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
+                && IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
+                && IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
+                && IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
+                && IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
+                && IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
+                && IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
+        }
 
-            if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
+        private bool IsRegexOrSubstringMatch(string input, string pattern)
+        {
+            if (string.IsNullOrEmpty(pattern))
             {
-                if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
-                {
-                    return false;
-                }
+                // In profile identification: An empty pattern matches anything.
+                return true;
             }
 
-            if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
+            if (string.IsNullOrEmpty(input))
             {
-                if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
-                {
-                    return false;
-                }
+                // The profile contains a value, and the device doesn't.
+                return false;
             }
 
-            return true;
-        }
-
-        private bool IsRegexOrSubstringMatch(string input, string pattern)
-        {
             try
             {
-                return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+                return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
+                    || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
             }
             catch (ArgumentException ex)
             {

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

@@ -21,6 +21,7 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <!-- Code Analyzers-->

+ 2 - 0
Emby.Dlna/EventSubscriptionResponse.cs

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

+ 2 - 0
Emby.Dlna/Eventing/DlnaEventManager.cs

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

+ 2 - 0
Emby.Dlna/Eventing/EventSubscription.cs

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

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -5,7 +7,6 @@ using System.Globalization;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Sockets;
-using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
 using Emby.Dlna.Ssdp;

+ 0 - 1
Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs

@@ -1,7 +1,6 @@
 using System.Collections.Generic;
 using Emby.Dlna.Common;
 using Emby.Dlna.Service;
-using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.MediaReceiverRegistrar
 {

+ 38 - 0
Emby.Dlna/PlayTo/Device.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -368,6 +370,42 @@ namespace Emby.Dlna.PlayTo
             RestartTimer(true);
         }
 
+        /*
+         * SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
+         * Without that information, the next track command on the device does not work.
+         */
+        public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
+        {
+            var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
+
+            url = url.Replace("&", "&amp;", StringComparison.Ordinal);
+
+            _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
+
+            var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
+            if (command == null)
+            {
+                return;
+            }
+
+            var dictionary = new Dictionary<string, string>
+            {
+                { "NextURI", url },
+                { "NextURIMetaData", CreateDidlMeta(metaData) }
+            };
+
+            var service = GetAvTransportService();
+
+            if (service == null)
+            {
+                throw new InvalidOperationException("Unable to find service");
+            }
+
+            var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
+                .ConfigureAwait(false);
+        }
+
         private static string CreateDidlMeta(string value)
         {
             if (string.IsNullOrEmpty(value))

+ 2 - 0
Emby.Dlna/PlayTo/DeviceInfo.cs

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

+ 2 - 0
Emby.Dlna/PlayTo/MediaChangedEventArgs.cs

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

+ 47 - 4
Emby.Dlna/PlayTo/PlayToController.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -102,6 +104,22 @@ namespace Emby.Dlna.PlayTo
             _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
         }
 
+        /*
+         * Send a message to the DLNA device to notify what is the next track in the playlist.
+         */
+        private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
+        {
+            if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
+            {
+                // The current playing item is indeed in the play list and we are not yet at the end of the playlist.
+                var nextItemIndex = currentPlayListItemIndex + 1;
+                var nextItem = _playlist[nextItemIndex];
+
+                // Send the SetNextAvTransport message.
+                await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
         private void OnDeviceUnavailable()
         {
             try
@@ -156,6 +174,15 @@ namespace Emby.Dlna.PlayTo
                 var newItemProgress = GetProgressInfo(streamInfo);
 
                 await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
+
+                // Send a message to the DLNA device to notify what is the next track in the playlist.
+                var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
+                if (currentItemIndex >= 0)
+                {
+                    _currentPlaylistIndex = currentItemIndex;
+                }
+
+                await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
             }
             catch (Exception ex)
             {
@@ -425,6 +452,11 @@ namespace Emby.Dlna.PlayTo
                     var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+
+                    // Send a message to the DLNA device to notify what is the next track in the play list.
+                    var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+                    await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
                     return;
                 }
 
@@ -499,8 +531,8 @@ namespace Emby.Dlna.PlayTo
 
             if (streamInfo.MediaType == DlnaProfileType.Audio)
             {
-                return new ContentFeatureBuilder(profile)
-                    .BuildAudioHeader(
+                return ContentFeatureBuilder.BuildAudioHeader(
+                        profile,
                         streamInfo.Container,
                         streamInfo.TargetAudioCodec.FirstOrDefault(),
                         streamInfo.TargetAudioBitrate,
@@ -514,8 +546,8 @@ namespace Emby.Dlna.PlayTo
 
             if (streamInfo.MediaType == DlnaProfileType.Video)
             {
-                var list = new ContentFeatureBuilder(profile)
-                    .BuildVideoHeader(
+                var list = ContentFeatureBuilder.BuildVideoHeader(
+                        profile,
                         streamInfo.Container,
                         streamInfo.TargetVideoCodec.FirstOrDefault(),
                         streamInfo.TargetAudioCodec.FirstOrDefault(),
@@ -623,6 +655,9 @@ namespace Emby.Dlna.PlayTo
 
             await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
 
+            // Send a message to the DLNA device to notify what is the next track in the play list.
+            await SendNextTrackMessage(index, cancellationToken);
+
             var streamInfo = currentitem.StreamInfo;
             if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
             {
@@ -736,6 +771,10 @@ namespace Emby.Dlna.PlayTo
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
 
+                    // Send a message to the DLNA device to notify what is the next track in the play list.
+                    var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+                    await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
                     if (EnableClientSideSeek(newItem.StreamInfo))
                     {
                         await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -761,6 +800,10 @@ namespace Emby.Dlna.PlayTo
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
 
+                    // Send a message to the DLNA device to notify what is the next track in the play list.
+                    var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+                    await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
                     if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
                     {
                         await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -188,7 +190,7 @@ namespace Emby.Dlna.PlayTo
 
                 _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
 
-                string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
+                string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
 
                 controller = new PlayToController(
                     sessionInfo,

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

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

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

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

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

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

+ 2 - 0
Emby.Dlna/PlayTo/PlaylistItem.cs

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

+ 2 - 0
Emby.Dlna/PlayTo/PlaylistItemFactory.cs

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

+ 2 - 1
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -1,8 +1,9 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
 using System.Globalization;
-using System.IO;
 using System.Net.Http;
 using System.Net.Mime;
 using System.Text;

+ 7 - 7
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -46,7 +46,7 @@ namespace Emby.Dlna.PlayTo
         {
             var serviceAction = new ServiceAction
             {
-                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
+                Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
             };
 
             var argumentList = serviceAction.ArgumentList;
@@ -68,9 +68,9 @@ namespace Emby.Dlna.PlayTo
 
             return new Argument
             {
-                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
-                Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
-                RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
+                Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+                Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty,
+                RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty
             };
         }
 
@@ -89,8 +89,8 @@ namespace Emby.Dlna.PlayTo
 
             return new StateVariable
             {
-                Name = container.GetValue(UPnpNamespaces.Svc + "name"),
-                DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
+                Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty,
+                DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty,
                 AllowedValues = allowedValues
             };
         }
@@ -166,7 +166,7 @@ namespace Emby.Dlna.PlayTo
             return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
         }
 
-        private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
+        private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
         {
             var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase));
 

+ 2 - 0
Emby.Dlna/PlayTo/uBaseObject.cs

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

+ 2 - 1
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -250,7 +250,8 @@ namespace Emby.Dlna.Server
 
             url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
 
-            return SecurityElement.Escape(url);
+            // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
+            return SecurityElement.Escape(url) ?? string.Empty;
         }
 
         private IEnumerable<DeviceIcon> GetIcons()

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

@@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
 
         private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
         {
-            ControlRequestInfo requestInfo = null;
+            ControlRequestInfo? requestInfo = null;
 
             using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
             {
@@ -151,7 +151,7 @@ namespace Emby.Dlna.Service
 
         private async Task<ControlRequestInfo> ParseBodyTagAsync(XmlReader reader)
         {
-            string namespaceURI = null, localName = null;
+            string? namespaceURI = null, localName = null;
 
             await reader.MoveToContentAsync().ConfigureAwait(false);
             await reader.ReadAsync().ConfigureAwait(false);

+ 3 - 1
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -104,7 +106,7 @@ namespace Emby.Dlna.Ssdp
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers,
-                    LocalIpAddress = e.LocalIpAddress
+                    RemoteIpAddress = e.RemoteIpAddress
                 });
 
             DeviceDiscoveredInternal?.Invoke(this, args);

+ 3 - 3
Emby.Dlna/Ssdp/SsdpExtensions.cs

@@ -7,21 +7,21 @@ namespace Emby.Dlna.Ssdp
 {
     public static class SsdpExtensions
     {
-        public static string GetValue(this XElement container, XName name)
+        public static string? GetValue(this XElement container, XName name)
         {
             var node = container.Element(name);
 
             return node?.Value;
         }
 
-        public static string GetAttributeValue(this XElement container, XName name)
+        public static string? GetAttributeValue(this XElement container, XName name)
         {
             var node = container.Attribute(name);
 
             return node?.Value;
         }
 
-        public static string GetDescendantValue(this XElement container, XName name)
+        public static string? GetDescendantValue(this XElement container, XName name)
             => container.Descendants(name).FirstOrDefault()?.Value;
     }
 }

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

@@ -1,7 +1,7 @@
 using System;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Naming.Audio
 {
@@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
         /// <returns>True if file at path is audio file.</returns>
         public static bool IsAudioFile(string path, NamingOptions options)
         {
-            var extension = Path.GetExtension(path);
-            return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
     }
 }

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

@@ -23,11 +23,12 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <Compile Include="..\SharedVersion.cs" />
+    <Compile Include="../SharedVersion.cs" />
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+    <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+    <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
   </ItemGroup>
 
   <PropertyGroup>

+ 5 - 0
Emby.Naming/TV/EpisodeResolver.cs

@@ -68,6 +68,11 @@ namespace Emby.Naming.TV
             var parsingResult = new EpisodePathParser(_options)
                 .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
 
+            if (!parsingResult.Success && !isStub)
+            {
+                return null;
+            }
+
             return new EpisodeInfo(path)
             {
                 Container = container,

+ 49 - 46
Emby.Naming/Video/ExtraResolver.cs

@@ -29,70 +29,73 @@ namespace Emby.Naming.Video
         /// <param name="path">Path to file.</param>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
         public ExtraResult GetExtraInfo(string path)
-        {
-            return _options.VideoExtraRules
-                .Select(i => GetExtraInfo(path, i))
-                .FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
-        }
-
-        private ExtraResult GetExtraInfo(string path, ExtraRule rule)
         {
             var result = new ExtraResult();
 
-            if (rule.MediaType == MediaType.Audio)
+            for (var i = 0; i < _options.VideoExtraRules.Length; i++)
             {
-                if (!AudioFileParser.IsAudioFile(path, _options))
+                var rule = _options.VideoExtraRules[i];
+                if (rule.MediaType == MediaType.Audio)
                 {
-                    return result;
+                    if (!AudioFileParser.IsAudioFile(path, _options))
+                    {
+                        continue;
+                    }
                 }
-            }
-            else if (rule.MediaType == MediaType.Video)
-            {
-                if (!new VideoResolver(_options).IsVideoFile(path))
+                else if (rule.MediaType == MediaType.Video)
                 {
-                    return result;
+                    if (!new VideoResolver(_options).IsVideoFile(path))
+                    {
+                        continue;
+                    }
                 }
-            }
-
-            if (rule.RuleType == ExtraRuleType.Filename)
-            {
-                var filename = Path.GetFileNameWithoutExtension(path);
 
-                if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
+                var pathSpan = path.AsSpan();
+                if (rule.RuleType == ExtraRuleType.Filename)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
-                }
-            }
-            else if (rule.RuleType == ExtraRuleType.Suffix)
-            {
-                var filename = Path.GetFileNameWithoutExtension(path);
+                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
 
-                if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
+                    if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
+                }
+                else if (rule.RuleType == ExtraRuleType.Suffix)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
+                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
+
+                    if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
                 }
-            }
-            else if (rule.RuleType == ExtraRuleType.Regex)
-            {
-                var filename = Path.GetFileName(path);
+                else if (rule.RuleType == ExtraRuleType.Regex)
+                {
+                    var filename = Path.GetFileName(path);
 
-                var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+                    var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
 
-                if (regex.IsMatch(filename))
+                    if (regex.IsMatch(filename))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
+                }
+                else if (rule.RuleType == ExtraRuleType.DirectoryName)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
+                    var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
+                    if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    {
+                        result.ExtraType = rule.ExtraType;
+                        result.Rule = rule;
+                    }
                 }
-            }
-            else if (rule.RuleType == ExtraRuleType.DirectoryName)
-            {
-                var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
-                if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
+
+                if (result.ExtraType != null)
                 {
-                    result.ExtraType = rule.ExtraType;
-                    result.Rule = rule;
+                    return result;
                 }
             }
 

+ 10 - 12
Emby.Naming/Video/VideoResolver.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Naming.Video
 {
@@ -59,15 +59,15 @@ namespace Emby.Naming.Video
             }
 
             bool isStub = false;
-            string? container = null;
+            ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
             string? stubType = null;
 
             if (!isDirectory)
             {
-                var extension = Path.GetExtension(path);
+                var extension = Path.GetExtension(path.AsSpan());
 
                 // Check supported extensions
-                if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+                if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                     // It's not supported. Check stub extensions
                     if (!StubResolver.TryResolveFile(path, _options, out stubType))
@@ -86,9 +86,7 @@ namespace Emby.Naming.Video
 
             var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
 
-            var name = isDirectory
-                ? Path.GetFileName(path)
-                : Path.GetFileNameWithoutExtension(path);
+            var name = Path.GetFileNameWithoutExtension(path);
 
             int? year = null;
 
@@ -107,7 +105,7 @@ namespace Emby.Naming.Video
 
             return new VideoFileInfo(
                 path: path,
-                container: container,
+                container: container.IsEmpty ? null : container.ToString(),
                 isStub: isStub,
                 name: name,
                 year: year,
@@ -126,8 +124,8 @@ namespace Emby.Naming.Video
         /// <returns>True if is video file.</returns>
         public bool IsVideoFile(string path)
         {
-            var extension = Path.GetExtension(path);
-            return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>
@@ -137,8 +135,8 @@ namespace Emby.Naming.Video
         /// <returns>True if is video file stub.</returns>
         public bool IsStubFile(string path)
         {
-            var extension = Path.GetExtension(path);
-            return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>

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

@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
             CachePath = cacheDirectoryPath;
             WebPath = webDirectoryPath;
 
-            DataPath = Path.Combine(ProgramDataPath, "data");
+            _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
         }
 
         /// <summary>
@@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
         /// Gets the folder path to the data directory.
         /// </summary>
         /// <value>The data directory.</value>
-        public string DataPath
-        {
-            get => _dataPath;
-            private set => _dataPath = Directory.CreateDirectory(value).FullName;
-        }
+        public string DataPath => _dataPath;
 
         /// <inheritdoc />
         public string VirtualDataPath => "%AppDataPath%";

+ 2 - 0
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

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

+ 0 - 3
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -1,9 +1,6 @@
-#nullable enable
-
 using System;
 using System.IO;
 using System.Linq;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Serialization;
 
 namespace Emby.Server.Implementations.AppBase

+ 5 - 15
Emby.Server.Implementations/ApplicationHost.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -335,10 +337,7 @@ namespace Emby.Server.Implementations
         {
             get
             {
-                if (_deviceId == null)
-                {
-                    _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
-                }
+                _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
 
                 return _deviceId.Value;
             }
@@ -370,10 +369,7 @@ namespace Emby.Server.Implementations
         /// <returns>System.Object.</returns>
         protected object CreateInstanceSafe(Type type)
         {
-            if (_creatingInstances == null)
-            {
-                _creatingInstances = new List<Type>();
-            }
+            _creatingInstances ??= new List<Type>();
 
             if (_creatingInstances.IndexOf(type) != -1)
             {
@@ -607,12 +603,8 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
 
-            // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
-
-            // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
             ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+            ServiceCollection.AddSingleton<EncodingHelper>();
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
             ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -677,8 +669,6 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 
-            ServiceCollection.AddSingleton<EncodingHelper>();
-
             ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
             ServiceCollection.AddSingleton<TranscodingJobHelper>();

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

@@ -1,9 +1,10 @@
+#nullable disable
+
 using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Text;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;

+ 2 - 2
Emby.Server.Implementations/Collections/CollectionImageProvider.cs

@@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
                     return null;
                 })
                 .Where(i => i != null)
-                .GroupBy(x => x.Id)
+                .GroupBy(x => x!.Id) // We removed the null values
                 .Select(x => x.First())
-                .ToList();
+                .ToList()!; // Again... the list doesn't contain any null values
         }
 
         /// <inheritdoc />

+ 36 - 34
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -1,6 +1,7 @@
+#nullable disable
+
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -8,11 +9,9 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
@@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
 
         private IEnumerable<BoxSet> GetCollections(User user)
         {
-            var folder = GetCollectionsFolder(false).Result;
+            var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
 
             return folder == null
                 ? Enumerable.Empty<BoxSet>()
@@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
 
                 parentFolder.AddChild(collection, CancellationToken.None);
 
-                if (options.ItemIdList.Length > 0)
+                if (options.ItemIdList.Count > 0)
                 {
                     await AddToCollectionAsync(
                         collection.Id,
@@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
 
                 if (fireEvent)
                 {
-                    ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
-                    {
-                        Collection = collection,
-                        ItemsChanged = itemList
-                    });
+                    ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
                 }
             }
         }
@@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
                 },
                 RefreshPriority.High);
 
-            ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
-            {
-                Collection = collection,
-                ItemsChanged = itemList
-            });
+            ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
         }
 
         /// <inheritdoc />
@@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
         {
             var results = new Dictionary<Guid, BaseItem>();
 
-            var allBoxsets = GetCollections(user).ToList();
+            var allBoxSets = GetCollections(user).ToList();
 
             foreach (var item in items)
             {
-                if (!(item is ISupportsBoxSetGrouping))
+                if (item is not ISupportsBoxSetGrouping)
                 {
                     results[item.Id] = item;
                 }
@@ -331,34 +322,45 @@ namespace Emby.Server.Implementations.Collections
                 {
                     var itemId = item.Id;
 
-                    var currentBoxSets = allBoxsets
-                        .Where(i => i.ContainsLinkedChildByItemId(itemId))
-                        .ToList();
-
-                    if (currentBoxSets.Count > 0)
+                    var itemIsInBoxSet = false;
+                    foreach (var boxSet in allBoxSets)
                     {
-                        foreach (var boxset in currentBoxSets)
+                        if (!boxSet.ContainsLinkedChildByItemId(itemId))
                         {
-                            results[boxset.Id] = boxset;
+                            continue;
                         }
+
+                        itemIsInBoxSet = true;
+
+                        results.TryAdd(boxSet.Id, boxSet);
                     }
-                    else
+
+                    // skip any item that is in a box set
+                    if (itemIsInBoxSet)
                     {
-                        var alreadyInResults = false;
-                        foreach (var child in item.GetMediaSources(true))
+                        continue;
+                    }
+
+                    var alreadyInResults = false;
+                    // 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)
+                    {
+                        foreach (var childId in video.GetLocalAlternateVersionIds())
                         {
-                            if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
+                            if (!results.ContainsKey(childId))
                             {
-                                alreadyInResults = true;
-                                break;
+                                continue;
                             }
-                        }
 
-                        if (!alreadyInResults)
-                        {
-                            results[item.Id] = item;
+                            alreadyInResults = true;
+                            break;
                         }
                     }
+
+                    if (!alreadyInResults)
+                    {
+                        results[itemId] = item;
+                    }
                 }
             }
 

+ 2 - 0
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using System.Globalization;
 using System.IO;

+ 0 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using Emby.Server.Implementations.HttpServer;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 
 namespace Emby.Server.Implementations

+ 0 - 2
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Collections.Generic;
 using System.Security.Cryptography;

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
 
             foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
             {
-                if (row[1].SQLiteType != SQLiteType.Null)
+                if (row.TryGetString(1, out var columnName))
                 {
-                    var name = row[1].ToString();
-
-                    columnNames.Add(name);
+                    columnNames.Add(columnName);
                 }
             }
 

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

@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
 {
     public class ManagedConnection : IDisposable
     {
-        private SQLiteDatabaseConnection _db;
+        private SQLiteDatabaseConnection? _db;
         private readonly SemaphoreSlim _writeLock;
         private bool _disposed = false;
 
@@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
             return _db.RunInTransaction(action, mode);
         }
 
-        public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
+        public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
         {
             return _db.Query(sql);
         }
 
-        public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
+        public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
         {
             return _db.Query(sql, values);
         }

+ 108 - 17
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -1,3 +1,4 @@
+#nullable disable
 #pragma warning disable CS1591
 
 using System;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Data
             });
         }
 
-        public static Guid ReadGuidFromBlob(this IResultSetValue result)
+        public static Guid ReadGuidFromBlob(this ResultSetValue result)
         {
             return new Guid(result.ToBlob());
         }
@@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Data
         private static string GetDateTimeKindFormat(DateTimeKind kind)
             => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
 
-        public static DateTime ReadDateTime(this IResultSetValue result)
+        public static DateTime ReadDateTime(this ResultSetValue result)
         {
             var dateText = result.ToString();
 
@@ -96,49 +97,139 @@ namespace Emby.Server.Implementations.Data
                 DateTimeStyles.None).ToUniversalTime();
         }
 
-        public static DateTime? TryReadDateTime(this IResultSetValue result)
+        public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
         {
-            var dateText = result.ToString();
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
+            }
+
+            var dateText = item.ToString();
 
             if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
             {
-                return dateTimeResult.ToUniversalTime();
+                result = dateTimeResult.ToUniversalTime();
+                return true;
+            }
+
+            result = default;
+            return false;
+        }
+
+        public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
+        {
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
             }
 
-            return null;
+            result = item.ReadGuidFromBlob();
+            return true;
         }
 
-        public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool IsDbNull(this ResultSetValue result)
         {
-            return result[index].SQLiteType == SQLiteType.Null;
+            return result.SQLiteType == SQLiteType.Null;
         }
 
-        public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
+        public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
         {
             return result[index].ToString();
         }
 
-        public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
+        {
+            result = null;
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                return false;
+            }
+
+            result = item.ToString();
+            return true;
+        }
+
+        public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
         {
             return result[index].ToBool();
         }
 
-        public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
+        {
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
+            }
+
+            result = item.ToBool();
+            return true;
+        }
+
+        public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
         {
-            return result[index].ToInt();
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
+            }
+
+            result = item.ToInt();
+            return true;
         }
 
-        public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
+        public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
         {
             return result[index].ToInt64();
         }
 
-        public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
+        {
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
+            }
+
+            result = item.ToInt64();
+            return true;
+        }
+
+        public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
+        {
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
+            }
+
+            result = item.ToFloat();
+            return true;
+        }
+
+        public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
         {
-            return result[index].ToFloat();
+            var item = reader[index];
+            if (item.IsDbNull())
+            {
+                result = default;
+                return false;
+            }
+
+            result = item.ToDouble();
+            return true;
         }
 
-        public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
+        public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
         {
             return result[index].ReadGuidFromBlob();
         }
@@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
+        public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
         {
             while (statement.MoveNext())
             {

文件差异内容过多而无法显示
+ 238 - 286
Emby.Server.Implementations/Data/SqliteItemRepository.cs


+ 11 - 9
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data
         /// Read a row from the specified reader into the provided userData object.
         /// </summary>
         /// <param name="reader"></param>
-        private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
+        private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
         {
             var userData = new UserItemData();
 
             userData.Key = reader[0].ToString();
             // userData.UserId = reader[1].ReadGuidFromBlob();
 
-            if (reader[2].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetDouble(2, out var rating))
             {
-                userData.Rating = reader[2].ToDouble();
+                userData.Rating = rating;
             }
 
             userData.Played = reader[3].ToBool();
@@ -365,19 +367,19 @@ namespace Emby.Server.Implementations.Data
             userData.IsFavorite = reader[5].ToBool();
             userData.PlaybackPositionTicks = reader[6].ToInt64();
 
-            if (reader[7].SQLiteType != SQLiteType.Null)
+            if (reader.TryReadDateTime(7, out var lastPlayedDate))
             {
-                userData.LastPlayedDate = reader[7].TryReadDateTime();
+                userData.LastPlayedDate = lastPlayedDate;
             }
 
-            if (reader[8].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetInt32(8, out var audioStreamIndex))
             {
-                userData.AudioStreamIndex = reader[8].ToInt();
+                userData.AudioStreamIndex = audioStreamIndex;
             }
 
-            if (reader[9].SQLiteType != SQLiteType.Null)
+            if (reader.TryGetInt32(9, out var subtitleStreamIndex))
             {
-                userData.SubtitleStreamIndex = reader[9].ToInt();
+                userData.SubtitleStreamIndex = subtitleStreamIndex;
             }
 
             return userData;

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

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
         /// This holds all the types in the running assemblies
         /// so that we can de-serialize properly when we don't have strong types.
         /// </summary>
-        private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
+        private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
 
         /// <summary>
         /// Gets the type.
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Data
         /// <param name="typeName">Name of the type.</param>
         /// <returns>Type.</returns>
         /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
-        public Type GetType(string typeName)
+        public Type? GetType(string typeName)
         {
             if (string.IsNullOrEmpty(typeName))
             {
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Data
         /// </summary>
         /// <param name="typeName">Name of the type.</param>
         /// <returns>Type.</returns>
-        private Type LookupType(string typeName)
+        private Type? LookupType(string typeName)
         {
             return AppDomain.CurrentDomain.GetAssemblies()
                 .Select(a => a.GetType(typeName))

+ 2 - 0
Emby.Server.Implementations/Devices/DeviceId.cs

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

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

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

+ 5 - 12
Emby.Server.Implementations/Dto/DtoService.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -665,10 +667,7 @@ namespace Emby.Server.Implementations.Dto
             var tag = GetImageCacheTag(item, image);
             if (!string.IsNullOrEmpty(image.BlurHash))
             {
-                if (dto.ImageBlurHashes == null)
-                {
-                    dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
-                }
+                dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
 
                 if (!dto.ImageBlurHashes.ContainsKey(image.Type))
                 {
@@ -702,10 +701,7 @@ namespace Emby.Server.Implementations.Dto
 
             if (hashes.Count > 0)
             {
-                if (dto.ImageBlurHashes == null)
-                {
-                    dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
-                }
+                dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
 
                 dto.ImageBlurHashes[imageType] = hashes;
             }
@@ -898,10 +894,7 @@ namespace Emby.Server.Implementations.Dto
                     dto.Taglines = new string[] { item.Tagline };
                 }
 
-                if (dto.Taglines == null)
-                {
-                    dto.Taglines = Array.Empty<string>();
-                }
+                dto.Taglines ??= Array.Empty<string>();
             }
 
             dto.Type = item.GetBaseItemKind();

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

@@ -9,6 +9,7 @@
     <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
     <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
     <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
+    <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -27,11 +28,11 @@
     <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.3" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.6" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
-    <PackageReference Include="sharpcompress" Version="0.28.1" />
-    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
+    <PackageReference Include="sharpcompress" Version="0.28.2" />
+    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.0.1" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
   </ItemGroup>
 
@@ -44,6 +45,7 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
     <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
     <NoWarn>AD0001</NoWarn>
     <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>

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

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

+ 2 - 0
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

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

+ 2 - 0
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

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

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

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Net.Sockets;
 using System.Threading;

+ 2 - 0
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

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

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

@@ -1,6 +1,5 @@
 #pragma warning disable CS1591
 
-using System;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;

+ 27 - 27
Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -2,8 +2,8 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Net;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         {
             if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
             {
-                return (AuthorizationInfo)cached;
+                return (AuthorizationInfo)cached!; // Cache should never contain null
             }
 
             return GetAuthorization(requestContext);
@@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
         }
 
         private AuthorizationInfo GetAuthorizationInfoFromDictionary(
-            in Dictionary<string, string> auth,
+            in Dictionary<string, string>? auth,
             in IHeaderDictionary headers,
             in IQueryCollection queryString)
         {
-            string deviceId = null;
-            string device = null;
-            string client = null;
-            string version = null;
-            string token = null;
+            string? deviceId = null;
+            string? device = null;
+            string? client = null;
+            string? version = null;
+            string? token = null;
 
             if (auth != null)
             {
@@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="httpReq">The HTTP req.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
+        private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
         {
             var auth = httpReq.Request.Headers["X-Emby-Authorization"];
 
@@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 auth = httpReq.Request.Headers[HeaderNames.Authorization];
             }
 
-            return GetAuthorization(auth);
+            return GetAuthorization(auth.Count > 0 ? auth[0] : null);
         }
 
         /// <summary>
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="httpReq">The HTTP req.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+        private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
         {
             var auth = httpReq.Headers["X-Emby-Authorization"];
 
@@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
                 auth = httpReq.Headers[HeaderNames.Authorization];
             }
 
-            return GetAuthorization(auth);
+            return GetAuthorization(auth.Count > 0 ? auth[0] : null);
         }
 
         /// <summary>
@@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security
         /// </summary>
         /// <param name="authorizationHeader">The authorization header.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private Dictionary<string, string> GetAuthorization(string authorizationHeader)
+        private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
         {
             if (authorizationHeader == null)
             {
                 return null;
             }
 
-            var parts = authorizationHeader.Split(' ', 2);
+            var firstSpace = authorizationHeader.IndexOf(' ');
 
-            // There should be at least to parts
-            if (parts.Length != 2)
+            // There should be at least two parts
+            if (firstSpace == -1)
             {
                 return null;
             }
 
-            var acceptedNames = new[] { "MediaBrowser", "Emby" };
+            var name = authorizationHeader[..firstSpace];
 
-            // It has to be a digest request
-            if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
+            if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
+                && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
             {
                 return null;
             }
 
-            // Remove uptil the first space
-            authorizationHeader = parts[1];
-            parts = authorizationHeader.Split(',');
+            authorizationHeader = authorizationHeader[(firstSpace + 1)..];
 
             var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-            foreach (var item in parts)
+            foreach (var item in authorizationHeader.Split(','))
             {
-                var param = item.Trim().Split('=', 2);
+                var trimmedItem = item.Trim();
+                var firstEqualsSign = trimmedItem.IndexOf('=');
 
-                if (param.Length == 2)
+                if (firstEqualsSign > 0)
                 {
-                    var value = NormalizeValue(param[1].Trim('"'));
-                    result[param[0]] = value;
+                    var key = trimmedItem[..firstEqualsSign].ToString();
+                    var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
+                    result[key] = value;
                 }
             }
 

+ 2 - 2
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -36,14 +36,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetSession((HttpContext)requestContext);
         }
 
-        public User GetUser(HttpContext requestContext)
+        public User? GetUser(HttpContext requestContext)
         {
             var session = GetSession(requestContext);
 
             return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
         }
 
-        public User GetUser(object requestContext)
+        public User? GetUser(object requestContext)
         {
             return GetUser(((HttpRequest)requestContext).HttpContext);
         }

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

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Buffers;
 using System.IO.Pipelines;

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

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

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

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

+ 2 - 0
Emby.Server.Implementations/IO/LibraryMonitor.cs

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

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

@@ -2,11 +2,10 @@
 
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Text;
+using System.Runtime.InteropServices;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.System;
@@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.IO
 
         private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
         private readonly string _tempPath;
-        private readonly bool _isEnvironmentCaseInsensitive;
+        private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
 
         public ManagedFileSystem(
             ILogger<ManagedFileSystem> logger,
@@ -32,8 +31,6 @@ namespace Emby.Server.Implementations.IO
         {
             Logger = logger;
             _tempPath = applicationPaths.TempDirectory;
-
-            _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
         }
 
         public virtual void AddShortcutHandler(IShortcutHandler handler)
@@ -55,7 +52,7 @@ namespace Emby.Server.Implementations.IO
             }
 
             var extension = Path.GetExtension(filename);
-            return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
+            return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
         }
 
         /// <summary>
@@ -64,7 +61,7 @@ namespace Emby.Server.Implementations.IO
         /// <param name="filename">The filename.</param>
         /// <returns>System.String.</returns>
         /// <exception cref="ArgumentNullException">filename</exception>
-        public virtual string ResolveShortcut(string filename)
+        public virtual string? ResolveShortcut(string filename)
         {
             if (string.IsNullOrEmpty(filename))
             {
@@ -72,7 +69,7 @@ namespace Emby.Server.Implementations.IO
             }
 
             var extension = Path.GetExtension(filename);
-            var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
+            var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
 
             return handler?.Resolve(filename);
         }
@@ -263,8 +260,6 @@ namespace Emby.Server.Implementations.IO
                             result.Exists = false;
                         }
                     }
-
-                    result.DirectoryName = fileInfo.DirectoryName;
                 }
 
                 result.CreationTimeUtc = GetCreationTimeUtc(info);
@@ -303,16 +298,37 @@ namespace Emby.Server.Implementations.IO
         /// <param name="filename">The filename.</param>
         /// <returns>System.String.</returns>
         /// <exception cref="ArgumentNullException">The filename is null.</exception>
-        public virtual string GetValidFilename(string filename)
+        public string GetValidFilename(string filename)
         {
-            var builder = new StringBuilder(filename);
-
-            foreach (var c in Path.GetInvalidFileNameChars())
+            var invalid = Path.GetInvalidFileNameChars();
+            var first = filename.IndexOfAny(invalid);
+            if (first == -1)
             {
-                builder = builder.Replace(c, ' ');
+                // Fast path for clean strings
+                return filename;
             }
 
-            return builder.ToString();
+            return string.Create(
+                filename.Length,
+                (filename, invalid, first),
+                (chars, state) =>
+                {
+                    state.filename.AsSpan().CopyTo(chars);
+
+                    chars[state.first++] = ' ';
+
+                    var len = chars.Length;
+                    foreach (var c in state.invalid)
+                    {
+                        for (int i = state.first; i < len; i++)
+                        {
+                            if (chars[i] == c)
+                            {
+                                chars[i] = ' ';
+                            }
+                        }
+                    }
+                });
         }
 
         /// <summary>
@@ -585,7 +601,7 @@ namespace Emby.Server.Implementations.IO
             return GetFiles(path, null, false, recursive);
         }
 
-        public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+        public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
             var enumerationOptions = GetEnumerationOptions(recursive);
 
@@ -639,7 +655,7 @@ namespace Emby.Server.Implementations.IO
             return GetFilePaths(path, null, false, recursive);
         }
 
-        public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+        public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
             var enumerationOptions = GetEnumerationOptions(recursive);
 
@@ -684,20 +700,5 @@ namespace Emby.Server.Implementations.IO
                 AttributesToSkip = 0
             };
         }
-
-        private static void RunProcess(string path, string args, string workingDirectory)
-        {
-            using (var process = Process.Start(new ProcessStartInfo
-            {
-                Arguments = args,
-                FileName = path,
-                CreateNoWindow = true,
-                WorkingDirectory = workingDirectory,
-                WindowStyle = ProcessWindowStyle.Normal
-            }))
-            {
-                process.WaitForExit();
-            }
-        }
     }
 }

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

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
 
         public string Extension => ".mblink";
 
-        public string Resolve(string shortcutPath)
+        public string? Resolve(string shortcutPath)
         {
             if (string.IsNullOrEmpty(shortcutPath))
             {

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

@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO
 {
     public class StreamHelper : IStreamHelper
     {
-        public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
+        public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken)
         {
             byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
             try

+ 0 - 2
Emby.Server.Implementations/IStartupOptions.cs

@@ -1,6 +1,4 @@
 #pragma warning disable CS1591
-#nullable enable
-using System;
 
 namespace Emby.Server.Implementations
 {

+ 0 - 8
Emby.Server.Implementations/Images/ArtistImageProvider.cs

@@ -2,20 +2,12 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.Images;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
 
 namespace Emby.Server.Implementations.Images
 {

+ 3 - 1
Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images
                 InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
             };
 
-            if (options.InputPaths.Length == 0)
+            if (options.InputPaths.Count == 0)
             {
                 return null;
             }

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

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

+ 2 - 1
Emby.Server.Implementations/Images/DynamicImageProvider.cs

@@ -1,10 +1,11 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using Emby.Server.Implementations.Images;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;

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

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

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

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

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System.Collections.Generic;
@@ -29,9 +31,7 @@ namespace Emby.Server.Implementations.Images
                 {
                     var subItem = i.Item2;
 
-                    var episode = subItem as Episode;
-
-                    if (episode != null)
+                    if (subItem is Episode episode)
                     {
                         var series = episode.Series;
                         if (series != null && series.HasImage(ImageType.Primary))

+ 2 - 0
Emby.Server.Implementations/Library/ExclusiveLiveStream.cs

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

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

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Linq;
 using DotNet.Globbing;

+ 84 - 11
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -48,6 +50,7 @@ using MediaBrowser.Providers.MediaInfo;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 using Genre = MediaBrowser.Controller.Entities.Genre;
 using Person = MediaBrowser.Controller.Entities.Person;
 using VideoResolver = Emby.Naming.Video.VideoResolver;
@@ -175,10 +178,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     lock (_rootFolderSyncLock)
                     {
-                        if (_rootFolder == null)
-                        {
-                            _rootFolder = CreateRootFolder();
-                        }
+                        _rootFolder ??= CreateRootFolder();
                     }
                 }
 
@@ -558,7 +558,6 @@ namespace Emby.Server.Implementations.Library
             var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
             {
                 Parent = parent,
-                Path = fullPath,
                 FileInfo = fileInfo,
                 CollectionType = collectionType,
                 LibraryOptions = libraryOptions
@@ -684,7 +683,7 @@ namespace Emby.Server.Implementations.Library
 
                         foreach (var item in items)
                         {
-                            ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService);
+                            ResolverHelper.SetInitialItemValues(item, parent, this, directoryService);
                         }
 
                         items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
@@ -1163,7 +1162,7 @@ namespace Emby.Server.Implementations.Library
                 progress.Report(percent * 100);
             }
 
-            _itemRepository.UpdateInheritedValues(cancellationToken);
+            _itemRepository.UpdateInheritedValues();
 
             progress.Report(100);
         }
@@ -2517,7 +2516,7 @@ namespace Emby.Server.Implementations.Library
         public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
         {
             var series = episode.Series;
-            bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
+            bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
             if (!isAbsoluteNaming.Value)
             {
                 // In other words, no filter applied
@@ -2529,9 +2528,23 @@ namespace Emby.Server.Implementations.Library
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 
             // TODO nullable - what are we trying to do there with empty episodeInfo?
-            var episodeInfo = episode.IsFileProtocol
-                ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path)
-                : new Naming.TV.EpisodeInfo(episode.Path);
+            EpisodeInfo episodeInfo = null;
+            if (episode.IsFileProtocol)
+            {
+                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
+                // Resolve from parent folder if it's not the Season folder
+                if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
+                {
+                    episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
+                    if (episodeInfo != null)
+                    {
+                        // add the container
+                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
+                    }
+                }
+            }
+
+            episodeInfo ??= new EpisodeInfo(episode.Path);
 
             try
             {
@@ -2880,6 +2893,12 @@ namespace Emby.Server.Implementations.Library
         }
 
         public void UpdatePeople(BaseItem item, List<PersonInfo> people)
+        {
+            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
+        }
+
+        /// <inheritdoc />
+        public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
         {
             if (!item.SupportsPeople)
             {
@@ -2887,6 +2906,8 @@ namespace Emby.Server.Implementations.Library
             }
 
             _itemRepository.UpdatePeople(item.Id, people);
+
+            await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
         }
 
         public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -2990,6 +3011,58 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
+        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
+        {
+            var personsToSave = new List<BaseItem>();
+
+            foreach (var person in people)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                var itemUpdateType = ItemUpdateType.MetadataDownload;
+                var saveEntity = false;
+                var personEntity = GetPerson(person.Name);
+
+                // if PresentationUniqueKey is empty it's likely a new item.
+                if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+                {
+                    personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
+                    saveEntity = true;
+                }
+
+                foreach (var id in person.ProviderIds)
+                {
+                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
+                    {
+                        personEntity.SetProviderId(id.Key, id.Value);
+                        saveEntity = true;
+                    }
+                }
+
+                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
+                {
+                    personEntity.SetImage(
+                        new ItemImageInfo
+                        {
+                            Path = person.ImageUrl,
+                            Type = ImageType.Primary
+                        },
+                        0);
+
+                    saveEntity = true;
+                    itemUpdateType = ItemUpdateType.ImageUpdate;
+                }
+
+                if (saveEntity)
+                {
+                    personsToSave.Add(personEntity);
+                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+                }
+            }
+
+            CreateItems(personsToSave, null, CancellationToken.None);
+        }
+
         private void StartScanInBackground()
         {
             Task.Run(() =>

+ 2 - 0
Emby.Server.Implementations/Library/LiveStreamHelper.cs

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

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -199,10 +201,15 @@ namespace Emby.Server.Implementations.Library
                     {
                         source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
                     }
+                    else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+                    {
+                        source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
+                        source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
+                    }
                 }
             }
 
-            return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
+            return SortMediaSources(list);
         }
 
         public MediaProtocol GetPathProtocol(string path)
@@ -436,7 +443,7 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+        private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
         {
             return sources.OrderBy(i =>
             {
@@ -451,8 +458,9 @@ namespace Emby.Server.Implementations.Library
             {
                 var stream = i.VideoStream;
 
-                return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+                return stream?.Width ?? 0;
             })
+            .Where(i => i.Type != MediaSourceType.Placeholder)
             .ToList();
         }
 
@@ -584,18 +592,9 @@ namespace Emby.Server.Implementations.Library
 
         public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
         {
-            var info = _openStreams.Values.FirstOrDefault(i =>
-            {
-                var liveStream = i as ILiveStream;
-                if (liveStream != null)
-                {
-                    return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
-                }
-
-                return false;
-            });
+            var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
 
-            return Task.FromResult(info as IDirectStreamProvider);
+            return Task.FromResult(info.Value as IDirectStreamProvider);
         }
 
         public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)

+ 2 - 0
Emby.Server.Implementations/Library/MediaStreamSelector.cs

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

部分文件因为文件数量过多而无法显示