Browse Source

Merge branch 'master' into comparisons

BaronGreenback 4 years ago
parent
commit
6648b7d7da
100 changed files with 1103 additions and 974 deletions
  1. 7 1
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 23 13
      .github/workflows/automation.yml
  3. 119 0
      .github/workflows/commands.yml
  4. 0 43
      .github/workflows/label-commenter-config.yml
  5. 0 22
      .github/workflows/label-commenter.yml
  6. 0 17
      .github/workflows/merge-conflicts.yml
  7. 0 27
      .github/workflows/rebase.yml
  8. 1 0
      .gitignore
  9. 3 1
      CONTRIBUTORS.md
  10. 3 3
      Dockerfile
  11. 3 3
      Dockerfile.arm
  12. 3 3
      Dockerfile.arm64
  13. 36 0
      Emby.Dlna/PlayTo/Device.cs
  14. 41 0
      Emby.Dlna/PlayTo/PlayToController.cs
  15. 3 3
      Emby.Naming/Audio/AudioFileParser.cs
  16. 3 2
      Emby.Naming/Emby.Naming.csproj
  17. 2 3
      Emby.Naming/TV/EpisodeResolver.cs
  18. 49 46
      Emby.Naming/Video/ExtraResolver.cs
  19. 0 53
      Emby.Naming/Video/FlagParser.cs
  20. 36 52
      Emby.Naming/Video/Format3DParser.cs
  21. 9 14
      Emby.Naming/Video/Format3DResult.cs
  22. 1 3
      Emby.Naming/Video/StackResolver.cs
  23. 4 3
      Emby.Naming/Video/VideoFileInfo.cs
  24. 139 94
      Emby.Naming/Video/VideoListResolver.cs
  25. 34 42
      Emby.Naming/Video/VideoResolver.cs
  26. 2 6
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  27. 26 20
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  28. 2 3
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  29. 2 0
      Emby.Server.Implementations/ApplicationHost.cs
  30. 2 0
      Emby.Server.Implementations/Channels/ChannelManager.cs
  31. 2 2
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  32. 5 11
      Emby.Server.Implementations/Collections/CollectionManager.cs
  33. 2 0
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  34. 0 2
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  35. 4 4
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  36. 3 3
      Emby.Server.Implementations/Data/ManagedConnection.cs
  37. 108 17
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  38. 204 293
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  39. 11 9
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  40. 5 15
      Emby.Server.Implementations/Data/TypeMapper.cs
  41. 2 0
      Emby.Server.Implementations/Devices/DeviceId.cs
  42. 2 0
      Emby.Server.Implementations/Devices/DeviceManager.cs
  43. 2 0
      Emby.Server.Implementations/Dto/DtoService.cs
  44. 6 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  45. 2 9
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  46. 2 0
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  47. 2 0
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  48. 2 4
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  49. 4 4
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  50. 27 27
      Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  51. 2 2
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  52. 0 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  53. 2 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  54. 2 0
      Emby.Server.Implementations/IO/FileRefresher.cs
  55. 2 0
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  56. 13 13
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  57. 1 1
      Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
  58. 1 1
      Emby.Server.Implementations/IO/StreamHelper.cs
  59. 0 1
      Emby.Server.Implementations/IStartupOptions.cs
  60. 3 1
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  61. 2 0
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  62. 2 0
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  63. 2 0
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  64. 2 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  65. 2 0
      Emby.Server.Implementations/Images/PlaylistImageProvider.cs
  66. 1 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  67. 2 0
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  68. 0 2
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  69. 32 28
      Emby.Server.Implementations/Library/LibraryManager.cs
  70. 2 0
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  71. 5 6
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  72. 2 0
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  73. 2 0
      Emby.Server.Implementations/Library/MusicManager.cs
  74. 0 2
      Emby.Server.Implementations/Library/PathExtensions.cs
  75. 0 2
      Emby.Server.Implementations/Library/ResolverHelper.cs
  76. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  77. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  78. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  79. 16 19
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  80. 2 0
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  81. 2 0
      Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
  82. 2 0
      Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
  83. 2 0
      Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
  84. 8 6
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  85. 2 0
      Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
  86. 2 0
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  87. 2 0
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  88. 2 0
      Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
  89. 2 0
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  90. 2 0
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  91. 2 0
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  92. 2 0
      Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
  93. 2 0
      Emby.Server.Implementations/Library/SearchEngine.cs
  94. 4 2
      Emby.Server.Implementations/Library/UserDataManager.cs
  95. 2 0
      Emby.Server.Implementations/Library/UserViewManager.cs
  96. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  97. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  98. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  99. 3 4
      Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
  100. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

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

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

+ 23 - 13
.github/workflows/automation.yml

@@ -1,26 +1,36 @@
 name: Automation
 
 on:
-  pull_request:
+  push:
+    branches:
+      - master
+  pull_request_target:
+  issue_comment:
 
 jobs:
-  main:
+  label:
+    name: Labeling
     runs-on: ubuntu-latest
     steps:
-      - name: Does PR has the stable backport label?
-        uses: Dreamcodeio/does-pr-has-label@v1.2
-        id: checkLabel
+      - name: Apply label
+        uses: eps1lon/actions-label-merge-conflict@v2.0.1
+        if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
         with:
-          label: stable backport
+          dirtyLabel: 'merge conflict'
+          repoToken: ${{ secrets.JF_BOT_TOKEN }}
 
+  project:
+    name: Project board
+    runs-on: ubuntu-latest
+    steps:
       - name: Remove from 'Current Release' project
         uses: alex-page/github-project-automation-plus@v0.7.1
-        if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
+        if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         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.7.1
@@ -29,16 +39,16 @@ jobs:
         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.7.1
-        if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
+        if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         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'
@@ -52,7 +62,7 @@ jobs:
         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.7.1
@@ -61,4 +71,4 @@ jobs:
         with:
           project: Issue Triage for Main Repo
           column: Pending response
-          repo-token: ${{ secrets.GH_TOKEN }}
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}

+ 119 - 0
.github/workflows/commands.yml

@@ -0,0 +1,119 @@
+name: Commands
+on:
+  issue_comment:
+    types:
+      - created
+      - edited
+  pull_request_target:
+    types:
+      - labeled
+      - synchronize
+
+jobs:
+  rebase:
+    name: Rebase
+    if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Notify as seen
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          comment-id: ${{ github.event.comment.id }}
+          reactions: '+1'
+
+      - name: Checkout the latest code
+        uses: actions/checkout@v2
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          fetch-depth: 0
+
+      - name: Automatic Rebase
+        uses: cirrus-actions/rebase@1.4
+        env:
+          GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+
+  check-backport:
+    name: Check Backport
+    if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: Notify as seen
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        if: ${{ github.event.comment != null }}
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          comment-id: ${{ github.event.comment.id }}
+          reactions: eyes
+
+      - name: Checkout the latest code
+        uses: actions/checkout@v2
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          fetch-depth: 0
+
+      - name: Notify as running
+        id: comment_running
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        if: ${{ github.event.comment != null }}
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          issue-number: ${{ github.event.issue.number }}
+          body: |
+            Running backport tests...
+
+      - name: Perform test backport
+        id: run_tests
+        run: |
+          set +o errexit
+          git config --global user.name "Jellyfin Bot"
+          git config --global user.email "team@jellyfin.org"
+          CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
+          git checkout master
+          git merge --no-ff ${CURRENT_BRANCH}
+          MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
+          git fetch --all
+          CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
+          stable_branch="Current stable release branch: ${CURRENT_STABLE}"
+          echo ${stable_branch}
+          echo ::set-output name=branch::${stable_branch}
+          git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
+          git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
+          retcode=$?
+          cat output.txt | grep -v 'hint:'
+          output="$( grep -v 'hint:'  output.txt )"
+          output="${output//'%'/'%25'}"
+          output="${output//$'\n'/'%0A'}"
+          output="${output//$'\r'/'%0D'}" 
+          echo ::set-output name=output::$output
+          exit ${retcode}
+
+      - name: Notify with result success
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        if: ${{ github.event.comment != null && success() }}
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          comment-id: ${{ steps.comment_running.outputs.comment-id }}
+          body: |
+            ${{ steps.run_tests.outputs.branch }}
+            Output from `git cherry-pick`:
+
+            ---
+
+            ${{ steps.run_tests.outputs.output }}
+          reactions: hooray
+
+      - name: Notify with result failure
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        if: ${{ github.event.comment != null && failure() }}
+        with:
+          token: ${{ secrets.JF_BOT_TOKEN }}
+          comment-id: ${{ steps.comment_running.outputs.comment-id }}
+          body: |
+            ${{ steps.run_tests.outputs.branch }}
+            Output from `git cherry-pick`:
+
+            ---
+
+            ${{ steps.run_tests.outputs.output }}
+          reactions: confused

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

@@ -1,43 +0,0 @@
-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.

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

@@ -1,22 +0,0 @@
-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

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

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

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

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

+ 1 - 0
.gitignore

@@ -268,6 +268,7 @@ doc/
 # Deployment artifacts
 dist
 *.exe
+*.dll
 
 # BenchmarkDotNet artifacts
 BenchmarkDotNet.Artifacts

+ 3 - 1
CONTRIBUTORS.md

@@ -70,6 +70,7 @@
  - [marius-luca-87](https://github.com/marius-luca-87)
  - [mark-monteiro](https://github.com/mark-monteiro)
  - [Matt07211](https://github.com/Matt07211)
+ - [Maxr1998](https://github.com/Maxr1998)
  - [mcarlton00](https://github.com/mcarlton00)
  - [mitchfizz05](https://github.com/mitchfizz05)
  - [MrTimscampi](https://github.com/MrTimscampi)
@@ -110,7 +111,7 @@
  - [sorinyo2004](https://github.com/sorinyo2004)
  - [sparky8251](https://github.com/sparky8251)
  - [spookbits](https://github.com/spookbits)
- - [ssenart] (https://github.com/ssenart)
+ - [ssenart](https://github.com/ssenart)
  - [stanionascu](https://github.com/stanionascu)
  - [stevehayles](https://github.com/stevehayles)
  - [SuperSandro2000](https://github.com/SuperSandro2000)
@@ -146,6 +147,7 @@
  - [nielsvanvelzen](https://github.com/nielsvanvelzen)
  - [skyfrk](https://github.com/skyfrk)
  - [ianjazz246](https://github.com/ianjazz246)
+ - [peterspenler](https://github.com/peterspenler)
 
 # Emby Contributors
 

+ 3 - 3
Dockerfile

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

+ 3 - 3
Dockerfile.arm

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

+ 3 - 3
Dockerfile.arm64

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

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

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

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

@@ -104,6 +104,22 @@ namespace Emby.Dlna.PlayTo
             _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
         }
 
+        /*
+         * Send a message to the DLNA device to notify what is the next track in the playlist.
+         */
+        private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
+        {
+            if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
+            {
+                // The current playing item is indeed in the play list and we are not yet at the end of the playlist.
+                var nextItemIndex = currentPlayListItemIndex + 1;
+                var nextItem = _playlist[nextItemIndex];
+
+                // Send the SetNextAvTransport message.
+                await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
         private void OnDeviceUnavailable()
         {
             try
@@ -158,6 +174,15 @@ namespace Emby.Dlna.PlayTo
                 var newItemProgress = GetProgressInfo(streamInfo);
 
                 await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
+
+                // Send a message to the DLNA device to notify what is the next track in the playlist.
+                var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
+                if (currentItemIndex >= 0)
+                {
+                    _currentPlaylistIndex = currentItemIndex;
+                }
+
+                await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
             }
             catch (Exception ex)
             {
@@ -427,6 +452,11 @@ namespace Emby.Dlna.PlayTo
                     var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
+
+                    // Send a message to the DLNA device to notify what is the next track in the play list.
+                    var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+                    await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
                     return;
                 }
 
@@ -625,6 +655,9 @@ namespace Emby.Dlna.PlayTo
 
             await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
 
+            // Send a message to the DLNA device to notify what is the next track in the play list.
+            await SendNextTrackMessage(index, cancellationToken);
+
             var streamInfo = currentitem.StreamInfo;
             if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
             {
@@ -738,6 +771,10 @@ namespace Emby.Dlna.PlayTo
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
 
+                    // Send a message to the DLNA device to notify what is the next track in the play list.
+                    var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+                    await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
                     if (EnableClientSideSeek(newItem.StreamInfo))
                     {
                         await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
@@ -763,6 +800,10 @@ namespace Emby.Dlna.PlayTo
 
                     await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
 
+                    // Send a message to the DLNA device to notify what is the next track in the play list.
+                    var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
+                    await SendNextTrackMessage(newItemIndex, CancellationToken.None);
+
                     if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
                     {
                         await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);

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

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

@@ -16,7 +16,7 @@ namespace Emby.Naming.TV
         /// <summary>
         /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
         /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
+        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
         public EpisodeResolver(NamingOptions options)
         {
             _options = options;
@@ -62,8 +62,7 @@ namespace Emby.Naming.TV
                 container = extension.TrimStart('.');
             }
 
-            var flags = new FlagParser(_options).GetFlags(path);
-            var format3DResult = new Format3DParser(_options).Parse(flags);
+            var format3DResult = Format3DParser.Parse(path, _options);
 
             var parsingResult = new EpisodePathParser(_options)
                 .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);

+ 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 (!VideoResolver.IsVideoFile(path, _options))
+                    {
+                        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;
                 }
             }
 

+ 0 - 53
Emby.Naming/Video/FlagParser.cs

@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using Emby.Naming.Common;
-
-namespace Emby.Naming.Video
-{
-    /// <summary>
-    /// Parses list of flags from filename based on delimiters.
-    /// </summary>
-    public class FlagParser
-    {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="FlagParser"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
-        public FlagParser(NamingOptions options)
-        {
-            _options = options;
-        }
-
-        /// <summary>
-        /// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
-        /// </summary>
-        /// <param name="path">Path to file.</param>
-        /// <returns>List of found flags.</returns>
-        public string[] GetFlags(string path)
-        {
-            return GetFlags(path, _options.VideoFlagDelimiters);
-        }
-
-        /// <summary>
-        /// Parses flags from filename based on delimiters.
-        /// </summary>
-        /// <param name="path">Path to file.</param>
-        /// <param name="delimiters">Delimiters used to extract flags.</param>
-        /// <returns>List of found flags.</returns>
-        public string[] GetFlags(string path, char[] delimiters)
-        {
-            if (string.IsNullOrEmpty(path))
-            {
-                return Array.Empty<string>();
-            }
-
-            // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
-
-            var file = Path.GetFileName(path);
-
-            return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
-        }
-    }
-}

+ 36 - 52
Emby.Naming/Video/Format3DParser.cs

@@ -1,45 +1,37 @@
 using System;
-using System.Linq;
 using Emby.Naming.Common;
 
 namespace Emby.Naming.Video
 {
     /// <summary>
-    /// Parste 3D format related flags.
+    /// Parse 3D format related flags.
     /// </summary>
-    public class Format3DParser
+    public static class Format3DParser
     {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Format3DParser"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
-        public Format3DParser(NamingOptions options)
-        {
-            _options = options;
-        }
+        // Static default result to save on allocation costs.
+        private static readonly Format3DResult _defaultResult = new (false, null);
 
         /// <summary>
         /// Parse 3D format related flags.
         /// </summary>
         /// <param name="path">Path to file.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Returns <see cref="Format3DResult"/> object.</returns>
-        public Format3DResult Parse(string path)
+        public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
         {
-            int oldLen = _options.VideoFlagDelimiters.Length;
-            var delimiters = new char[oldLen + 1];
-            _options.VideoFlagDelimiters.CopyTo(delimiters, 0);
+            int oldLen = namingOptions.VideoFlagDelimiters.Length;
+            Span<char> delimiters = stackalloc char[oldLen + 1];
+            namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
             delimiters[oldLen] = ' ';
 
-            return Parse(new FlagParser(_options).GetFlags(path, delimiters));
+            return Parse(path, delimiters, namingOptions);
         }
 
-        internal Format3DResult Parse(string[] videoFlags)
+        private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
         {
-            foreach (var rule in _options.Format3DRules)
+            foreach (var rule in namingOptions.Format3DRules)
             {
-                var result = Parse(videoFlags, rule);
+                var result = Parse(path, rule, delimiters);
 
                 if (result.Is3D)
                 {
@@ -47,51 +39,43 @@ namespace Emby.Naming.Video
                 }
             }
 
-            return new Format3DResult();
+            return _defaultResult;
         }
 
-        private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
+        private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
         {
-            var result = new Format3DResult();
+            bool is3D = false;
+            string? format3D = null;
 
-            if (string.IsNullOrEmpty(rule.PrecedingToken))
+            // If there's no preceding token we just consider it found
+            var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
+            while (path.Length > 0)
             {
-                result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
-                result.Is3D = !string.IsNullOrEmpty(result.Format3D);
-
-                if (result.Is3D)
+                var index = path.IndexOfAny(delimiters);
+                if (index == -1)
                 {
-                    result.Tokens.Add(rule.Token);
+                    index = path.Length - 1;
                 }
-            }
-            else
-            {
-                var foundPrefix = false;
-                string? format = null;
 
-                foreach (var flag in videoFlags)
-                {
-                    if (foundPrefix)
-                    {
-                        result.Tokens.Add(rule.PrecedingToken);
+                var currentSlice = path[..index];
+                path = path[(index + 1)..];
 
-                        if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
-                        {
-                            format = flag;
-                            result.Tokens.Add(rule.Token);
-                        }
+                if (!foundPrefix)
+                {
+                    foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+                    continue;
+                }
 
-                        break;
-                    }
+                is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
 
-                    foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
+                if (is3D)
+                {
+                    format3D = rule.Token;
+                    break;
                 }
-
-                result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
-                result.Format3D = format;
             }
 
-            return result;
+            return is3D ? new Format3DResult(true, format3D) : _defaultResult;
         }
     }
 }

+ 9 - 14
Emby.Naming/Video/Format3DResult.cs

@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
 namespace Emby.Naming.Video
 {
     /// <summary>
@@ -10,27 +8,24 @@ namespace Emby.Naming.Video
         /// <summary>
         /// Initializes a new instance of the <see cref="Format3DResult"/> class.
         /// </summary>
-        public Format3DResult()
+        /// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
+        /// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
+        public Format3DResult(bool is3D, string? format3D)
         {
-            Tokens = new List<string>();
+            Is3D = is3D;
+            Format3D = format3D;
         }
 
         /// <summary>
-        /// Gets or sets a value indicating whether [is3 d].
+        /// Gets a value indicating whether [is3 d].
         /// </summary>
         /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
-        public bool Is3D { get; set; }
+        public bool Is3D { get; }
 
         /// <summary>
-        /// Gets or sets the format3 d.
+        /// Gets the format3 d.
         /// </summary>
         /// <value>The format3 d.</value>
-        public string? Format3D { get; set; }
-
-        /// <summary>
-        /// Gets or sets the tokens.
-        /// </summary>
-        /// <value>The tokens.</value>
-        public List<string> Tokens { get; set; }
+        public string? Format3D { get; }
     }
 }

+ 1 - 3
Emby.Naming/Video/StackResolver.cs

@@ -85,10 +85,8 @@ namespace Emby.Naming.Video
         /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
         public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
         {
-            var resolver = new VideoResolver(_options);
-
             var list = files
-                .Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
+                .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
                 .OrderBy(i => i.FullName)
                 .ToList();
 

+ 4 - 3
Emby.Naming/Video/VideoFileInfo.cs

@@ -1,3 +1,4 @@
+using System;
 using MediaBrowser.Model.Entities;
 
 namespace Emby.Naming.Video
@@ -106,9 +107,9 @@ namespace Emby.Naming.Video
         /// Gets the file name without extension.
         /// </summary>
         /// <value>The file name without extension.</value>
-        public string FileNameWithoutExtension => !IsDirectory
-            ? System.IO.Path.GetFileNameWithoutExtension(Path)
-            : System.IO.Path.GetFileName(Path);
+        public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
+            ? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
+            : System.IO.Path.GetFileName(Path.AsSpan());
 
         /// <inheritdoc />
         public override string ToString()

+ 139 - 94
Emby.Naming/Video/VideoListResolver.cs

@@ -12,31 +12,19 @@ namespace Emby.Naming.Video
     /// <summary>
     /// Resolves alternative versions and extras from list of video files.
     /// </summary>
-    public class VideoListResolver
+    public static class VideoListResolver
     {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="VideoListResolver"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
-        public VideoListResolver(NamingOptions options)
-        {
-            _options = options;
-        }
-
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
         /// </summary>
         /// <param name="files">List of related video files.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
-        public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
+        public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
         {
-            var videoResolver = new VideoResolver(_options);
-
             var videoInfos = files
-                .Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
+                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
                 .OfType<VideoFileInfo>()
                 .ToList();
 
@@ -46,7 +34,7 @@ namespace Emby.Naming.Video
                 .Where(i => i.ExtraType == null)
                 .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
 
-            var stackResult = new StackResolver(_options)
+            var stackResult = new StackResolver(namingOptions)
                 .Resolve(nonExtras).ToList();
 
             var remainingFiles = videoInfos
@@ -59,23 +47,17 @@ namespace Emby.Naming.Video
             {
                 var info = new VideoInfo(stack.Name)
                 {
-                    Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
+                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
                         .OfType<VideoFileInfo>()
                         .ToList()
                 };
 
                 info.Year = info.Files[0].Year;
 
-                var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
-
-                var extras = GetExtras(remainingFiles, extraBaseNames);
+                var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
 
                 if (extras.Count > 0)
                 {
-                    remainingFiles = remainingFiles
-                        .Except(extras)
-                        .ToList();
-
                     info.Extras = extras;
                 }
 
@@ -88,15 +70,12 @@ namespace Emby.Naming.Video
 
             foreach (var media in standaloneMedia)
             {
-                var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
+                var info = new VideoInfo(media.Name) { Files = new[] { media } };
 
                 info.Year = info.Files[0].Year;
 
-                var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
-
-                remainingFiles = remainingFiles
-                    .Except(extras.Concat(new[] { media }))
-                    .ToList();
+                remainingFiles.Remove(media);
+                var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
 
                 info.Extras = extras;
 
@@ -105,8 +84,7 @@ namespace Emby.Naming.Video
 
             if (supportMultiVersion)
             {
-                list = GetVideosGroupedByVersion(list)
-                    .ToList();
+                list = GetVideosGroupedByVersion(list, namingOptions);
             }
 
             // If there's only one resolved video, use the folder name as well to find extras
@@ -114,19 +92,14 @@ namespace Emby.Naming.Video
             {
                 var info = list[0];
                 var videoPath = list[0].Files[0].Path;
-                var parentPath = Path.GetDirectoryName(videoPath);
+                var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
 
-                if (!string.IsNullOrEmpty(parentPath))
+                if (!parentPath.IsEmpty)
                 {
                     var folderName = Path.GetFileName(parentPath);
-                    if (!string.IsNullOrEmpty(folderName))
+                    if (!folderName.IsEmpty)
                     {
-                        var extras = GetExtras(remainingFiles, new List<string> { folderName });
-
-                        remainingFiles = remainingFiles
-                            .Except(extras)
-                            .ToList();
-
+                        var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
                         extras.AddRange(info.Extras);
                         info.Extras = extras;
                     }
@@ -164,96 +137,168 @@ namespace Emby.Naming.Video
             // Whatever files are left, just add them
             list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
             {
-                Files = new List<VideoFileInfo> { i },
+                Files = new[] { i },
                 Year = i.Year
             }));
 
             return list;
         }
 
-        private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
+        private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
         {
             if (videos.Count == 0)
             {
                 return videos;
             }
 
-            var list = new List<VideoInfo>();
-
-            var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
+            var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
 
-            if (!string.IsNullOrEmpty(folderName)
-                && folderName.Length > 1
-                && videos.All(i => i.Files.Count == 1
-                    && IsEligibleForMultiVersion(folderName, i.Files[0].Path))
-                    && HaveSameYear(videos))
+            if (folderName.Length <= 1 || !HaveSameYear(videos))
             {
-                var ordered = videos.OrderBy(i => i.Name).ToList();
-
-                list.Add(ordered[0]);
+                return videos;
+            }
 
-                var alternateVersionsLen = ordered.Count - 1;
-                var alternateVersions = new VideoFileInfo[alternateVersionsLen];
-                for (int i = 0; i < alternateVersionsLen; i++)
+            // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+            for (var i = 0; i < videos.Count; i++)
+            {
+                var video = videos[i];
+                if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
                 {
-                    alternateVersions[i] = ordered[i + 1].Files[0];
+                    return videos;
                 }
+            }
+
+            // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
+            videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
 
-                list[0].AlternateVersions = alternateVersions;
-                list[0].Name = folderName;
-                var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
-                extras.AddRange(list[0].Extras);
-                list[0].Extras = extras;
+            var list = new List<VideoInfo>
+            {
+                videos[0]
+            };
 
-                return list;
+            var alternateVersionsLen = videos.Count - 1;
+            var alternateVersions = new VideoFileInfo[alternateVersionsLen];
+            var extras = new List<VideoFileInfo>(list[0].Extras);
+            for (int i = 0; i < alternateVersionsLen; i++)
+            {
+                var video = videos[i + 1];
+                alternateVersions[i] = video.Files[0];
+                extras.AddRange(video.Extras);
             }
 
-            return videos;
-        }
+            list[0].AlternateVersions = alternateVersions;
+            list[0].Name = folderName.ToString();
+            list[0].Extras = extras;
 
-        private bool HaveSameYear(List<VideoInfo> videos)
-        {
-            return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
+            return list;
         }
 
-        private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
+        private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
         {
-            string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
-            if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+            if (videos.Count == 1)
             {
-                // Remove the folder name before cleaning as we don't care about cleaning that part
-                if (folderName.Length <= testFilename.Length)
-                {
-                    testFilename = testFilename.Substring(folderName.Length).Trim();
-                }
+                return true;
+            }
 
-                if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+            var firstYear = videos[0].Year ?? -1;
+            for (var i = 1; i < videos.Count; i++)
+            {
+                if ((videos[i].Year ?? -1) != firstYear)
                 {
-                    testFilename = cleanName.Trim().ToString();
+                    return false;
                 }
+            }
 
-                // The CleanStringParser should have removed common keywords etc.
-                return string.IsNullOrEmpty(testFilename)
-                       || testFilename[0] == '-'
-                       || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
+            return true;
+        }
+
+        private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
+        {
+            var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
+            if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
             }
 
-            return false;
+            // Remove the folder name before cleaning as we don't care about cleaning that part
+            if (folderName.Length <= testFilename.Length)
+            {
+                testFilename = testFilename[folderName.Length..].Trim();
+            }
+
+            // There are no span overloads for regex unfortunately
+            var tmpTestFilename = testFilename.ToString();
+            if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+            {
+                tmpTestFilename = cleanName.Trim().ToString();
+            }
+
+            // The CleanStringParser should have removed common keywords etc.
+            return string.IsNullOrEmpty(tmpTestFilename)
+                   || testFilename[0] == '-'
+                   || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+        }
+
+        private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
         }
 
-        private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
+        private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
         {
-            foreach (var name in baseNames.ToList())
+            if (baseName.IsEmpty)
             {
-                var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
-                baseNames.Add(trimmedName);
+                return false;
             }
 
-            return remainingFiles
-                .Where(i => i.ExtraType != null)
-                .Where(i => baseNames.Any(b =>
-                    i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
-                .ToList();
+            return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
+                   || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
+        }
+
+        /// <summary>
+        /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
+        /// </summary>
+        /// <param name="remainingFiles">The list of remaining filenames.</param>
+        /// <param name="baseName">The base name to use for the comparison.</param>
+        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+        /// <returns>A list of video extras for [baseName].</returns>
+        private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
+        }
+
+        /// <summary>
+        /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
+        /// </summary>
+        /// <param name="remainingFiles">The list of remaining filenames.</param>
+        /// <param name="firstBaseName">The first base name to use for the comparison.</param>
+        /// <param name="secondBaseName">The second base name to use for the comparison.</param>
+        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+        /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
+        private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
+            var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
+
+            var result = new List<VideoFileInfo>();
+            for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
+            {
+                var file = remainingFiles[pos];
+                if (file.ExtraType == null)
+                {
+                    continue;
+                }
+
+                var filename = file.FileNameWithoutExtension;
+                if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
+                    || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
+                {
+                    result.Add(file);
+                    remainingFiles.RemoveAt(pos);
+                }
+            }
+
+            return result;
         }
     }
 }

+ 34 - 42
Emby.Naming/Video/VideoResolver.cs

@@ -1,46 +1,36 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Naming.Video
 {
     /// <summary>
     /// Resolves <see cref="VideoFileInfo"/> from file path.
     /// </summary>
-    public class VideoResolver
+    public static class VideoResolver
     {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="VideoResolver"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
-        /// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
-        public VideoResolver(NamingOptions options)
-        {
-            _options = options;
-        }
-
         /// <summary>
         /// Resolves the directory.
         /// </summary>
         /// <param name="path">The path.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>VideoFileInfo.</returns>
-        public VideoFileInfo? ResolveDirectory(string? path)
+        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
         {
-            return Resolve(path, true);
+            return Resolve(path, true, namingOptions);
         }
 
         /// <summary>
         /// Resolves the file.
         /// </summary>
         /// <param name="path">The path.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>VideoFileInfo.</returns>
-        public VideoFileInfo? ResolveFile(string? path)
+        public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
         {
-            return Resolve(path, false);
+            return Resolve(path, false, namingOptions);
         }
 
         /// <summary>
@@ -48,10 +38,11 @@ namespace Emby.Naming.Video
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <param name="parseName">Whether or not the name should be parsed for info.</param>
         /// <returns>VideoFileInfo.</returns>
         /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
-        public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
+        public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
         {
             if (string.IsNullOrEmpty(path))
             {
@@ -59,18 +50,18 @@ 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 (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                     // It's not supported. Check stub extensions
-                    if (!StubResolver.TryResolveFile(path, _options, out stubType))
+                    if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
                     {
                         return null;
                     }
@@ -81,25 +72,22 @@ namespace Emby.Naming.Video
                 container = extension.TrimStart('.');
             }
 
-            var flags = new FlagParser(_options).GetFlags(path);
-            var format3DResult = new Format3DParser(_options).Parse(flags);
+            var format3DResult = Format3DParser.Parse(path, namingOptions);
 
-            var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
+            var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
 
-            var name = isDirectory
-                ? Path.GetFileName(path)
-                : Path.GetFileNameWithoutExtension(path);
+            var name = Path.GetFileNameWithoutExtension(path);
 
             int? year = null;
 
             if (parseName)
             {
-                var cleanDateTimeResult = CleanDateTime(name);
+                var cleanDateTimeResult = CleanDateTime(name, namingOptions);
                 name = cleanDateTimeResult.Name;
                 year = cleanDateTimeResult.Year;
 
                 if (extraResult.ExtraType == null
-                    && TryCleanString(name, out ReadOnlySpan<char> newName))
+                    && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
                 {
                     name = newName.ToString();
                 }
@@ -107,7 +95,7 @@ namespace Emby.Naming.Video
 
             return new VideoFileInfo(
                 path: path,
-                container: container,
+                container: container.IsEmpty ? null : container.ToString(),
                 isStub: isStub,
                 name: name,
                 year: year,
@@ -123,43 +111,47 @@ namespace Emby.Naming.Video
         /// Determines if path is video file based on extension.
         /// </summary>
         /// <param name="path">Path to file.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>True if is video file.</returns>
-        public bool IsVideoFile(string path)
+        public static bool IsVideoFile(string path, NamingOptions namingOptions)
         {
-            var extension = Path.GetExtension(path);
-            return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>
         /// Determines if path is video file stub based on extension.
         /// </summary>
         /// <param name="path">Path to file.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>True if is video file stub.</returns>
-        public bool IsStubFile(string path)
+        public static bool IsStubFile(string path, NamingOptions namingOptions)
         {
-            var extension = Path.GetExtension(path);
-            return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            var extension = Path.GetExtension(path.AsSpan());
+            return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
 
         /// <summary>
         /// Tries to clean name of clutter.
         /// </summary>
         /// <param name="name">Raw name.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <param name="newName">Clean name.</param>
         /// <returns>True if cleaning of name was successful.</returns>
-        public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
+        public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
         {
-            return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
+            return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
         }
 
         /// <summary>
         /// Tries to get name and year from raw name.
         /// </summary>
         /// <param name="name">Raw name.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
-        public CleanDateTimeResult CleanDateTime(string name)
+        public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
         {
-            return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
+            return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
         }
     }
 }

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

+ 26 - 20
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
 
         private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
 
+        /// <summary>
+        /// The _configuration sync lock.
+        /// </summary>
+        private readonly object _configurationSyncLock = new object();
+
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
 
@@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
         /// </summary>
         private bool _configurationLoaded;
 
-        /// <summary>
-        /// The _configuration sync lock.
-        /// </summary>
-        private readonly object _configurationSyncLock = new object();
-
         /// <summary>
         /// The _configuration.
         /// </summary>
@@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
         /// <inheritdoc />
         public object GetConfiguration(string key)
         {
-            return _configurations.GetOrAdd(key, k =>
-            {
-                var file = GetConfigurationFile(key);
+            return _configurations.GetOrAdd(
+                key,
+                (k, configurationManager) =>
+                {
+                    var file = configurationManager.GetConfigurationFile(k);
 
-                var configurationInfo = _configurationStores
-                    .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+                    var configurationInfo = Array.Find(
+                        configurationManager._configurationStores,
+                        i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
 
-                if (configurationInfo == null)
-                {
-                    throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
-                }
+                    if (configurationInfo == null)
+                    {
+                        throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
+                    }
 
-                var configurationType = configurationInfo.ConfigurationType;
+                    var configurationType = configurationInfo.ConfigurationType;
 
-                lock (_configurationSyncLock)
-                {
-                    return LoadConfiguration(file, configurationType);
-                }
-            });
+                    lock (configurationManager._configurationSyncLock)
+                    {
+                        return configurationManager.LoadConfiguration(file, configurationType);
+                    }
+                },
+                this);
         }
 
         private object LoadConfiguration(string path, Type configurationType)

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

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.IO;
 using System.Linq;
@@ -35,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
             }
             catch (Exception)
             {
-                configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
+                // Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
+                configuration = Activator.CreateInstance(type)!;
             }
 
             using var stream = new MemoryStream(buffer?.Length ?? 0);

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

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

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

+ 5 - 15
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,26 +21,16 @@ 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))
             {
                 throw new ArgumentNullException(nameof(typeName));
             }
 
-            return _typeMap.GetOrAdd(typeName, LookupType);
-        }
-
-        /// <summary>
-        /// Lookups the type.
-        /// </summary>
-        /// <param name="typeName">Name of the type.</param>
-        /// <returns>Type.</returns>
-        private Type LookupType(string typeName)
-        {
-            return AppDomain.CurrentDomain.GetAssemblies()
-                .Select(a => a.GetType(typeName))
-                .FirstOrDefault(t => t != null);
+            return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
+                .Select(a => a.GetType(k))
+                .FirstOrDefault(t => t != null));
         }
     }
 }

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

+ 2 - 0
Emby.Server.Implementations/Dto/DtoService.cs

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

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

@@ -9,6 +9,7 @@
     <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
     <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
     <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
+    <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -27,11 +28,11 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.0.0" />
-    <PackageReference Include="sharpcompress" Version="0.28.2" />
-    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.0" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
+    <PackageReference Include="sharpcompress" Version="0.28.3" />
+    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.2" />
   </ItemGroup>
 
@@ -44,6 +45,7 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
     <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
     <NoWarn>AD0001</NoWarn>
     <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
             NatUtility.StartDiscovery();
 
             _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
-
-            _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
         }
 
         private void Stop()
@@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
             NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
 
             _timer?.Dispose();
-
-            _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
-        }
-
-        private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
-        {
-            NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
         }
 
         private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)

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

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

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Net.Sockets;
 using System.Threading;
@@ -56,8 +54,8 @@ namespace Emby.Server.Implementations.EntryPoints
 
             try
             {
-                _udpServer = new UdpServer(_logger, _appHost, _config);
-                _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+                _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
+                _udpServer.Start(_cancellationTokenSource.Token);
             }
             catch (SocketException ex)
             {

+ 4 - 4
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
 
         private readonly object _syncLock = new object();
-        private Timer _updateTimer;
+        private Timer? _updateTimer;
 
         public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
         {
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
             return Task.CompletedTask;
         }
 
-        void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
+        private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
         {
             if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
             {
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
                     _updateTimer.Change(UpdateDuration, Timeout.Infinite);
                 }
 
-                if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
+                if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
                 {
                     keys = new List<BaseItem>();
                     _changedItems[e.UserId] = keys;
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
-        private void UpdateTimerCallback(object state)
+        private void UpdateTimerCallback(object? state)
         {
             lock (_syncLock)
             {

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

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

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
@@ -61,7 +62,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))
             {
@@ -243,8 +244,8 @@ namespace Emby.Server.Implementations.IO
                 {
                     result.Length = fileInfo.Length;
 
-                    // Issue #2354 get the size of files behind symbolic links
-                    if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+                    // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
+                    if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
                     {
                         try
                         {
@@ -601,7 +602,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);
 
@@ -618,13 +619,13 @@ namespace Emby.Server.Implementations.IO
             {
                 files = files.Where(i =>
                 {
-                    var ext = i.Extension;
-                    if (ext == null)
+                    var ext = i.Extension.AsSpan();
+                    if (ext.IsEmpty)
                     {
                         return false;
                     }
 
-                    return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+                    return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
                 });
             }
 
@@ -636,8 +637,7 @@ namespace Emby.Server.Implementations.IO
             var directoryInfo = new DirectoryInfo(path);
             var enumerationOptions = GetEnumerationOptions(recursive);
 
-            return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
-                .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
+            return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
         }
 
         private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -655,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);
 
@@ -672,13 +672,13 @@ namespace Emby.Server.Implementations.IO
             {
                 files = files.Where(i =>
                 {
-                    var ext = Path.GetExtension(i);
-                    if (ext == null)
+                    var ext = Path.GetExtension(i.AsSpan());
+                    if (ext.IsEmpty)
                     {
                         return false;
                     }
 
-                    return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
+                    return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
                 });
             }
 

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

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

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

+ 1 - 1
Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
                 if (parent != null)
                 {
                     // Don't resolve these into audio files
-                    if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
+                    if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
                         && _libraryManager.IsAudioFile(filename))
                     {
                         return true;

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

+ 32 - 28
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -694,25 +696,32 @@ namespace Emby.Server.Implementations.Library
         }
 
         private IEnumerable<BaseItem> ResolveFileList(
-            IEnumerable<FileSystemMetadata> fileList,
+            IReadOnlyList<FileSystemMetadata> fileList,
             IDirectoryService directoryService,
             Folder parent,
             string collectionType,
             IItemResolver[] resolvers,
             LibraryOptions libraryOptions)
         {
-            return fileList.Select(f =>
+            // Given that fileList is a list we can save enumerator allocations by indexing
+            for (var i = 0; i < fileList.Count; i++)
             {
+                var file = fileList[i];
+                BaseItem result = null;
                 try
                 {
-                    return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
+                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error resolving path {path}", f.FullName);
-                    return null;
+                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
+                }
+
+                if (result != null)
+                {
+                    yield return result;
                 }
-            }).Where(i => i != null);
+            }
         }
 
         /// <summary>
@@ -1063,17 +1072,17 @@ namespace Emby.Server.Implementations.Library
             // Start by just validating the children of the root, but go no further
             await RootFolder.ValidateChildren(
                 new SimpleProgress<double>(),
-                cancellationToken,
                 new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
-                recursive: false).ConfigureAwait(false);
+                recursive: false,
+                cancellationToken).ConfigureAwait(false);
 
             await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
 
             await GetUserRootFolder().ValidateChildren(
                 new SimpleProgress<double>(),
-                cancellationToken,
                 new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
-                recursive: false).ConfigureAwait(false);
+                recursive: false,
+                cancellationToken).ConfigureAwait(false);
 
             // Quickly scan CollectionFolders for changes
             foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
@@ -1093,7 +1102,7 @@ namespace Emby.Server.Implementations.Library
             innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
 
             // Validate the entire media library
-            await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
+            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
 
             progress.Report(96);
 
@@ -2074,7 +2083,7 @@ namespace Emby.Server.Implementations.Library
                 return new List<Folder>();
             }
 
-            return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList());
+            return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
         }
 
         public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
@@ -2099,10 +2108,10 @@ namespace Emby.Server.Implementations.Library
             return GetCollectionFoldersInternal(item, allUserRootChildren);
         }
 
-        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren)
+        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
         {
             return allUserRootChildren
-                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
+                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
                 .ToList();
         }
 
@@ -2110,9 +2119,9 @@ namespace Emby.Server.Implementations.Library
         {
             if (!(item is CollectionFolder collectionFolder))
             {
+                // List.Find is more performant than FirstOrDefault due to enumerator allocation
                 collectionFolder = GetCollectionFolders(item)
-                   .OfType<CollectionFolder>()
-                   .FirstOrDefault();
+                    .Find(folder => folder is CollectionFolder) as CollectionFolder;
             }
 
             return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
@@ -2498,8 +2507,7 @@ namespace Emby.Server.Implementations.Library
         /// <inheritdoc />
         public bool IsVideoFile(string path)
         {
-            var resolver = new VideoResolver(GetNamingOptions());
-            return resolver.IsVideoFile(path);
+            return VideoResolver.IsVideoFile(path, GetNamingOptions());
         }
 
         /// <inheritdoc />
@@ -2677,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
             return changed;
         }
 
+        /// <inheritdoc />
         public NamingOptions GetNamingOptions()
         {
             if (_namingOptions == null)
@@ -2690,13 +2699,12 @@ namespace Emby.Server.Implementations.Library
 
         public ItemLookupInfo ParseName(string name)
         {
-            var resolver = new VideoResolver(GetNamingOptions());
-
-            var result = resolver.CleanDateTime(name);
+            var namingOptions = GetNamingOptions();
+            var result = VideoResolver.CleanDateTime(name, namingOptions);
 
             return new ItemLookupInfo
             {
-                Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
+                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
                 Year = result.Year
             };
         }
@@ -2710,9 +2718,7 @@ namespace Emby.Server.Implementations.Library
                 .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
                 .ToList();
 
-            var videoListResolver = new VideoListResolver(namingOptions);
-
-            var videos = videoListResolver.Resolve(fileSystemChildren);
+            var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
 
             var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 
@@ -2756,9 +2762,7 @@ namespace Emby.Server.Implementations.Library
                 .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
                 .ToList();
 
-            var videoListResolver = new VideoListResolver(namingOptions);
-
-            var videos = videoListResolver.Resolve(fileSystemChildren);
+            var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
 
             var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
 

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

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

+ 5 - 6
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -350,7 +352,7 @@ namespace Emby.Server.Implementations.Library
 
         private string[] NormalizeLanguage(string language)
         {
-            if (language == null)
+            if (string.IsNullOrEmpty(language))
             {
                 return Array.Empty<string>();
             }
@@ -379,8 +381,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
-                ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
+            var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
 
             var defaultAudioIndex = source.DefaultAudioStreamIndex;
             var audioLangage = defaultAudioIndex == null
@@ -409,9 +410,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
-                ? Array.Empty<string>()
-                : NormalizeLanguage(user.AudioLanguagePreference);
+            var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
 
             source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
         }

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

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

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

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

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

@@ -1,5 +1,3 @@
-#nullable enable
-
 using System;
 using System.Diagnostics.CodeAnalysis;
 using MediaBrowser.Common.Providers;

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

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

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

+ 16 - 19
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -45,11 +47,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
         protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
               where TVideoType : Video, new()
         {
-            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+            var namingOptions = LibraryManager.GetNamingOptions();
 
             // If the path is a file check for a matching extensions
-            var parser = new VideoResolver(namingOptions);
-
             if (args.IsDirectory)
             {
                 TVideoType video = null;
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     {
                         if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
                         {
-                            videoInfo = parser.ResolveDirectory(args.Path);
+                            videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 
                             if (videoInfo == null)
                             {
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
                         if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
                         {
-                            videoInfo = parser.ResolveDirectory(args.Path);
+                            videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 
                             if (videoInfo == null)
                             {
@@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     }
                     else if (IsDvdFile(filename))
                     {
-                        videoInfo = parser.ResolveDirectory(args.Path);
+                        videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 
                         if (videoInfo == null)
                         {
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
             }
             else
             {
-                var videoInfo = parser.Resolve(args.Path, false, false);
+                var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
 
                 if (videoInfo == null)
                 {
@@ -165,13 +165,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 +193,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;
                 }
@@ -250,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
         protected void Set3DFormat(Video video)
         {
-            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
-            var resolver = new Format3DParser(namingOptions);
-            var result = resolver.Parse(video.Path);
+            var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
 
             Set3DFormat(video, result.Is3D, result.Format3D);
         }

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

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

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;

+ 2 - 0
Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using System.IO;
 using MediaBrowser.Controller.Entities;

+ 8 - 6
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -1,9 +1,12 @@
+#nullable disable
+
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Video;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -255,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 }
             }
 
-            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
+            var namingOptions = LibraryManager.GetNamingOptions();
 
-            var resolver = new VideoListResolver(namingOptions);
-            var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
+            var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
 
             var result = new MultiItemResolverResult
             {
@@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return returnVideo;
         }
 
-        private bool IsInvalid(Folder parent, string collectionType)
+        private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
         {
             if (parent != null)
             {
@@ -545,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 }
             }
 
-            if (string.IsNullOrEmpty(collectionType))
+            if (collectionType.IsEmpty)
             {
                 return false;
             }
 
-            return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
+            return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
         }
     }
 }

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

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;

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

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

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

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

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

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

+ 2 - 0
Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System;
 using System.Linq;
 using MediaBrowser.Controller.Entities;

+ 2 - 0
Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 using System.Globalization;
 using Emby.Naming.TV;
 using MediaBrowser.Controller.Entities.TV;

+ 2 - 0
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

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

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

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

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

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

+ 4 - 2
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -220,7 +222,7 @@ namespace Emby.Server.Implementations.Library
             var hasRuntime = runtimeTicks > 0;
 
             // If a position has been reported, and if we know the duration
-            if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
+            if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
             {
                 var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 
@@ -239,7 +241,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     // Enforce MinResumeDuration
                     var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
-                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
+                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
                     {
                         positionTicks = 0;
                         data.Played = playedToCompletion = true;

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

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

+ 2 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

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

+ 2 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

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

+ 2 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

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

+ 3 - 4
Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Controller.LiveTv;
 
 namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
-
     internal class EpgChannelData
     {
 
@@ -39,13 +38,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
-        public ChannelInfo GetChannelById(string id)
+        public ChannelInfo? GetChannelById(string id)
             => _channelsById.GetValueOrDefault(id);
 
-        public ChannelInfo GetChannelByNumber(string number)
+        public ChannelInfo? GetChannelByNumber(string number)
             => _channelsByNumber.GetValueOrDefault(number);
 
-        public ChannelInfo GetChannelByName(string name)
+        public ChannelInfo? GetChannelByName(string name)
             => _channelsByName.GetValueOrDefault(name);
 
         public static string NormalizeName(string value)

+ 2 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

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

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