Browse Source

Merge branch 'master' into IsRoot_fix

BaronGreenback 4 years ago
parent
commit
51fb6e1d2d
100 changed files with 906 additions and 763 deletions
  1. 0 59
      .ci/azure-pipelines-api-client.yml
  2. 0 3
      .ci/azure-pipelines.yml
  3. 43 0
      .github/label-commenter-config.yml
  4. 10 12
      .github/workflows/automation.yml
  5. 22 0
      .github/workflows/label-commenter.yml
  6. 1 1
      .github/workflows/merge-conflicts.yml
  7. 3 3
      .github/workflows/rebase.yml
  8. 3 3
      Dockerfile
  9. 3 3
      Dockerfile.arm
  10. 3 3
      Dockerfile.arm64
  11. 2 0
      Emby.Dlna/Configuration/DlnaOptions.cs
  12. 0 1
      Emby.Dlna/ConfigurationExtension.cs
  13. 2 0
      Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
  14. 2 0
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  15. 2 0
      Emby.Dlna/ControlRequest.cs
  16. 2 0
      Emby.Dlna/ControlResponse.cs
  17. 21 7
      Emby.Dlna/Didl/DidlBuilder.cs
  18. 1 1
      Emby.Dlna/Didl/StringWriterWithEncoding.cs
  19. 0 1
      Emby.Dlna/DlnaConfigurationFactory.cs
  20. 32 65
      Emby.Dlna/DlnaManager.cs
  21. 1 0
      Emby.Dlna/Emby.Dlna.csproj
  22. 2 0
      Emby.Dlna/EventSubscriptionResponse.cs
  23. 2 0
      Emby.Dlna/Eventing/DlnaEventManager.cs
  24. 2 0
      Emby.Dlna/Eventing/EventSubscription.cs
  25. 2 0
      Emby.Dlna/Main/DlnaEntryPoint.cs
  26. 2 0
      Emby.Dlna/PlayTo/Device.cs
  27. 2 0
      Emby.Dlna/PlayTo/DeviceInfo.cs
  28. 2 0
      Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
  29. 2 0
      Emby.Dlna/PlayTo/PlayToController.cs
  30. 3 1
      Emby.Dlna/PlayTo/PlayToManager.cs
  31. 2 0
      Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
  32. 2 0
      Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
  33. 2 0
      Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
  34. 2 0
      Emby.Dlna/PlayTo/PlaylistItem.cs
  35. 2 0
      Emby.Dlna/PlayTo/PlaylistItemFactory.cs
  36. 2 0
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  37. 7 7
      Emby.Dlna/PlayTo/TransportCommands.cs
  38. 2 0
      Emby.Dlna/PlayTo/uBaseObject.cs
  39. 2 1
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  40. 2 2
      Emby.Dlna/Service/BaseControlHandler.cs
  41. 3 1
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  42. 3 3
      Emby.Dlna/Ssdp/SsdpExtensions.cs
  43. 3 3
      Emby.Naming/Audio/AudioFileParser.cs
  44. 3 2
      Emby.Naming/Emby.Naming.csproj
  45. 5 0
      Emby.Naming/TV/EpisodeResolver.cs
  46. 49 46
      Emby.Naming/Video/ExtraResolver.cs
  47. 10 12
      Emby.Naming/Video/VideoResolver.cs
  48. 2 6
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  49. 2 0
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  50. 0 2
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  51. 4 8
      Emby.Server.Implementations/ApplicationHost.cs
  52. 2 0
      Emby.Server.Implementations/Channels/ChannelManager.cs
  53. 2 2
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  54. 5 11
      Emby.Server.Implementations/Collections/CollectionManager.cs
  55. 2 0
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  56. 0 2
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  57. 4 4
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  58. 3 3
      Emby.Server.Implementations/Data/ManagedConnection.cs
  59. 108 17
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  60. 238 286
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  61. 11 9
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  62. 3 3
      Emby.Server.Implementations/Data/TypeMapper.cs
  63. 2 0
      Emby.Server.Implementations/Devices/DeviceId.cs
  64. 2 0
      Emby.Server.Implementations/Devices/DeviceManager.cs
  65. 5 12
      Emby.Server.Implementations/Dto/DtoService.cs
  66. 5 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  67. 2 0
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  68. 2 0
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  69. 2 0
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  70. 0 2
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  71. 2 0
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  72. 27 27
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  73. 2 2
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  74. 0 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  75. 2 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  76. 2 0
      Emby.Server.Implementations/IO/FileRefresher.cs
  77. 2 0
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  78. 34 33
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  79. 1 1
      Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
  80. 1 1
      Emby.Server.Implementations/IO/StreamHelper.cs
  81. 0 1
      Emby.Server.Implementations/IStartupOptions.cs
  82. 3 1
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  83. 2 0
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  84. 2 0
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  85. 2 0
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  86. 2 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  87. 3 3
      Emby.Server.Implementations/Images/PlaylistImageProvider.cs
  88. 2 0
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  89. 0 2
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  90. 84 11
      Emby.Server.Implementations/Library/LibraryManager.cs
  91. 2 0
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  92. 4 11
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  93. 2 0
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  94. 3 2
      Emby.Server.Implementations/Library/MusicManager.cs
  95. 8 4
      Emby.Server.Implementations/Library/PathExtensions.cs
  96. 19 43
      Emby.Server.Implementations/Library/ResolverHelper.cs
  97. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  98. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  99. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  100. 10 8
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.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

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

+ 10 - 12
.github/workflows/automation.yml

@@ -2,8 +2,6 @@ name: Automation
 
 on:
   pull_request:
-  issues:
-  issue_comment:
 
 jobs:
   main:
@@ -16,31 +14,31 @@ jobs:
           label: stable backport
 
       - name: Remove from 'Current Release' project
-        uses: alex-page/github-project-automation-plus@v0.5.1
+        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.GH_TOKEN }}
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
       - name: Add to 'Release Next' project
-        uses: alex-page/github-project-automation-plus@v0.5.1
+        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.GH_TOKEN }}
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
       - name: Add to 'Current Release' project
-        uses: alex-page/github-project-automation-plus@v0.5.1
+        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.GH_TOKEN }}
+          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'
@@ -48,19 +46,19 @@ jobs:
         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.5.1
+        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.GH_TOKEN }}
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
       - name: Add issue to triage project
-        uses: alex-page/github-project-automation-plus@v0.5.1
+        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.GH_TOKEN }}
+          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

+ 1 - 1
.github/workflows/merge-conflicts.yml

@@ -14,4 +14,4 @@ jobs:
       - uses: eps1lon/actions-label-merge-conflict@v2.0.1
         with:
           dirtyLabel: 'merge conflict'
-          repoToken: ${{ secrets.GH_TOKEN }}
+          repoToken: ${{ secrets.JF_BOT_TOKEN }}

+ 3 - 3
.github/workflows/rebase.yml

@@ -11,17 +11,17 @@ jobs:
       - name: Notify as seen
         uses: peter-evans/create-or-update-comment@v1.4.5
         with:
-          token: ${{ secrets.GH_TOKEN }}
+          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.GH_TOKEN }}
+          token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
 
       - name: Automatic Rebase
         uses: cirrus-actions/rebase@1.4
         env:
-          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

+ 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 - 0
Emby.Dlna/ContentDirectory/ControlHandler.cs

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

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

+ 21 - 7
Emby.Dlna/Didl/DidlBuilder.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -976,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)
@@ -1207,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 - 0
Emby.Dlna/Main/DlnaEntryPoint.cs

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

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

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

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

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

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

+ 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 - 0
Emby.Dlna/PlayTo/SsdpHttpClient.cs

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

+ 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 - 2
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

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

+ 4 - 8
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)
             {

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

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

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

+ 5 - 11
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -164,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,
@@ -248,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));
                 }
             }
         }
@@ -304,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 />

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

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

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

File diff suppressed because it is too large
+ 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();

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

@@ -27,11 +27,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 +44,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;

+ 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 - 1
Emby.Server.Implementations/IStartupOptions.cs

@@ -1,5 +1,4 @@
 #pragma warning disable CS1591
-#nullable enable
 
 namespace Emby.Server.Implementations
 {

+ 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 - 0
Emby.Server.Implementations/Images/DynamicImageProvider.cs

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

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

+ 4 - 11
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -590,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;

+ 3 - 2
Emby.Server.Implementations/Library/MusicManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -100,8 +102,7 @@ namespace Emby.Server.Implementations.Library
 
         public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
         {
-            var genre = item as MusicGenre;
-            if (genre != null)
+            if (item is MusicGenre genre)
             {
                 return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
             }

+ 8 - 4
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Common.Providers;
@@ -96,8 +94,14 @@ namespace Emby.Server.Implementations.Library
             // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
             // when the sub path matches a similar but in-complete subpath
             var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
-            if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)
-                || (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar))
+            if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            if (path.Length > subPath.Length
+                && !oldSubPathEndsWithSeparator
+                && path[subPath.Length] != newDirectorySeparatorChar)
             {
                 return false;
             }

+ 19 - 43
Emby.Server.Implementations/Library/ResolverHelper.cs

@@ -18,11 +18,10 @@ namespace Emby.Server.Implementations.Library
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="parent">The parent.</param>
-        /// <param name="fileSystem">The file system.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="directoryService">The directory service.</param>
-        /// <exception cref="ArgumentException">Item must have a path</exception>
-        public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService)
+        /// <exception cref="ArgumentException">Item must have a path.</exception>
+        public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService)
         {
             // This version of the below method has no ItemResolveArgs, so we have to require the path already being set
             if (string.IsNullOrEmpty(item.Path))
@@ -43,9 +42,14 @@ namespace Emby.Server.Implementations.Library
 
             // Make sure DateCreated and DateModified have values
             var fileInfo = directoryService.GetFile(item.Path);
-            SetDateCreated(item, fileSystem, fileInfo);
+            if (fileInfo == null)
+            {
+                throw new FileNotFoundException("Can't find item path.", item.Path);
+            }
 
-            EnsureName(item, item.Path, fileInfo);
+            SetDateCreated(item, fileInfo);
+
+            EnsureName(item, fileInfo);
         }
 
         /// <summary>
@@ -72,9 +76,9 @@ namespace Emby.Server.Implementations.Library
             item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
 
             // Make sure the item has a name
-            EnsureName(item, item.Path, args.FileInfo);
+            EnsureName(item, args.FileInfo);
 
-            item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+            item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
                 item.GetParents().Any(i => i.IsLocked);
 
             // Make sure DateCreated and DateModified have values
@@ -84,28 +88,15 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Ensures the name.
         /// </summary>
-        private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo)
+        private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo)
         {
             // If the subclass didn't supply a name, add it here
-            if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath))
+            if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
             {
-                var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name;
-
-                item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory);
+                item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name);
             }
         }
 
-        /// <summary>
-        /// Gets the display name.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
-        /// <returns>System.String.</returns>
-        private static string GetDisplayName(string path, bool isDirectory)
-        {
-            return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
-        }
-
         /// <summary>
         /// Ensures DateCreated and DateModified have values.
         /// </summary>
@@ -114,21 +105,6 @@ namespace Emby.Server.Implementations.Library
         /// <param name="args">The args.</param>
         private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
         {
-            if (fileSystem == null)
-            {
-                throw new ArgumentNullException(nameof(fileSystem));
-            }
-
-            if (item == null)
-            {
-                throw new ArgumentNullException(nameof(item));
-            }
-
-            if (args == null)
-            {
-                throw new ArgumentNullException(nameof(args));
-            }
-
             // See if a different path came out of the resolver than what went in
             if (!fileSystem.AreEqual(args.Path, item.Path))
             {
@@ -136,7 +112,7 @@ namespace Emby.Server.Implementations.Library
 
                 if (childData != null)
                 {
-                    SetDateCreated(item, fileSystem, childData);
+                    SetDateCreated(item, childData);
                 }
                 else
                 {
@@ -144,17 +120,17 @@ namespace Emby.Server.Implementations.Library
 
                     if (fileData.Exists)
                     {
-                        SetDateCreated(item, fileSystem, fileData);
+                        SetDateCreated(item, fileData);
                     }
                 }
             }
             else
             {
-                SetDateCreated(item, fileSystem, args.FileInfo);
+                SetDateCreated(item, args.FileInfo);
             }
         }
 
-        private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info)
+        private static void SetDateCreated(BaseItem item, FileSystemMetadata? info)
         {
             var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
 
@@ -163,7 +139,7 @@ namespace Emby.Server.Implementations.Library
                 // directoryService.getFile may return null
                 if (info != null)
                 {
-                    var dateCreated = fileSystem.GetCreationTimeUtc(info);
+                    var dateCreated = info.CreationTimeUtc;
 
                     if (dateCreated.Equals(DateTime.MinValue))
                     {

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

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

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

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

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using System.Linq;
 using System.Threading.Tasks;

+ 10 - 8
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -165,13 +167,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
         protected void SetVideoType(Video video, VideoFileInfo videoInfo)
         {
-            var extension = Path.GetExtension(video.Path);
-            video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
-              VideoType.Iso :
-              VideoType.VideoFile;
+            var extension = Path.GetExtension(video.Path.AsSpan());
+            video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
+                              || extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
+                ? VideoType.Iso
+                : VideoType.VideoFile;
 
-            video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+            video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
             video.IsPlaceHolder = videoInfo.IsStub;
 
             if (videoInfo.IsStub)
@@ -193,11 +195,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
         {
             if (video.VideoType == VideoType.Iso)
             {
-                if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
+                if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
                 {
                     video.IsoType = IsoType.Dvd;
                 }
-                else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
+                else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
                 {
                     video.IsoType = IsoType.BluRay;
                 }

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