소스 검색

Merge branch 'master' into authenticationdb-efcore

# Conflicts:
#	Emby.Server.Implementations/Devices/DeviceManager.cs
#	Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
#	Emby.Server.Implementations/Security/AuthenticationRepository.cs
#	Emby.Server.Implementations/Session/SessionManager.cs
#	Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
#	MediaBrowser.Controller/Library/IUserManager.cs
#	MediaBrowser.Controller/Net/ISessionContext.cs
Patrick Barron 4 년 전
부모
커밋
be88efce3c
100개의 변경된 파일997개의 추가작업 그리고 783개의 파일을 삭제
  1. 7 1
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 0 43
      .github/label-commenter-config.yml
  3. 18 8
      .github/workflows/automation.yml
  4. 119 0
      .github/workflows/commands.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. 36 0
      Emby.Dlna/PlayTo/Device.cs
  11. 41 0
      Emby.Dlna/PlayTo/PlayToController.cs
  12. 2 3
      Emby.Naming/TV/EpisodeResolver.cs
  13. 1 1
      Emby.Naming/Video/ExtraResolver.cs
  14. 0 53
      Emby.Naming/Video/FlagParser.cs
  15. 36 52
      Emby.Naming/Video/Format3DParser.cs
  16. 9 14
      Emby.Naming/Video/Format3DResult.cs
  17. 1 3
      Emby.Naming/Video/StackResolver.cs
  18. 4 3
      Emby.Naming/Video/VideoFileInfo.cs
  19. 139 94
      Emby.Naming/Video/VideoListResolver.cs
  20. 27 33
      Emby.Naming/Video/VideoResolver.cs
  21. 2 6
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  22. 26 20
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  23. 2 3
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  24. 2 0
      Emby.Server.Implementations/ApplicationHost.cs
  25. 2 0
      Emby.Server.Implementations/Channels/ChannelManager.cs
  26. 2 2
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  27. 2 0
      Emby.Server.Implementations/Collections/CollectionManager.cs
  28. 2 0
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  29. 0 2
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  30. 2 0
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  31. 3 3
      Emby.Server.Implementations/Data/ManagedConnection.cs
  32. 17 17
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  33. 294 227
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  34. 3 1
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  35. 5 15
      Emby.Server.Implementations/Data/TypeMapper.cs
  36. 2 0
      Emby.Server.Implementations/Devices/DeviceId.cs
  37. 2 0
      Emby.Server.Implementations/Dto/DtoService.cs
  38. 5 3
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  39. 2 9
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  40. 2 0
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  41. 2 0
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  42. 2 4
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  43. 4 4
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  44. 2 2
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  45. 0 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  46. 2 0
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  47. 2 0
      Emby.Server.Implementations/IO/FileRefresher.cs
  48. 2 0
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  49. 13 13
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  50. 1 1
      Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
  51. 1 1
      Emby.Server.Implementations/IO/StreamHelper.cs
  52. 0 1
      Emby.Server.Implementations/IStartupOptions.cs
  53. 2 0
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  54. 2 0
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  55. 2 0
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  56. 2 0
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  57. 2 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  58. 2 0
      Emby.Server.Implementations/Images/PlaylistImageProvider.cs
  59. 1 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  60. 2 0
      Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
  61. 0 2
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  62. 32 28
      Emby.Server.Implementations/Library/LibraryManager.cs
  63. 2 0
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  64. 5 6
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  65. 2 0
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  66. 2 0
      Emby.Server.Implementations/Library/MusicManager.cs
  67. 0 2
      Emby.Server.Implementations/Library/PathExtensions.cs
  68. 0 2
      Emby.Server.Implementations/Library/ResolverHelper.cs
  69. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  70. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  71. 2 0
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  72. 8 11
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  73. 2 0
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  74. 2 0
      Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
  75. 2 0
      Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
  76. 2 0
      Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
  77. 8 6
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  78. 2 0
      Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
  79. 2 0
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  80. 2 0
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  81. 2 0
      Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
  82. 2 0
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  83. 2 0
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  84. 2 0
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  85. 2 0
      Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
  86. 2 0
      Emby.Server.Implementations/Library/SearchEngine.cs
  87. 4 2
      Emby.Server.Implementations/Library/UserDataManager.cs
  88. 2 0
      Emby.Server.Implementations/Library/UserViewManager.cs
  89. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
  90. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  91. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  92. 3 4
      Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
  93. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  94. 2 0
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  95. 2 0
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  96. 2 0
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  97. 6 4
      Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
  98. 2 0
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  99. 6 4
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  100. 2 0
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.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**

+ 0 - 43
.github/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.

+ 18 - 8
.github/workflows/automation.yml

@@ -1,21 +1,31 @@
 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
@@ -33,7 +43,7 @@ jobs:
 
       - 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

+ 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 - 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.JF_BOT_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.JF_BOT_TOKEN }}
-          comment-id: ${{ github.event.comment.id }}
-          reactions: '+1'
-
-      - name: Checkout the latest code
-        uses: actions/checkout@v2
-        with:
-          token: ${{ secrets.JF_BOT_TOKEN }}
-          fetch-depth: 0
-
-      - name: Automatic Rebase
-        uses: cirrus-actions/rebase@1.4
-        env:
-          GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

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

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

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

+ 1 - 1
Emby.Naming/Video/ExtraResolver.cs

@@ -44,7 +44,7 @@ namespace Emby.Naming.Video
                 }
                 else if (rule.MediaType == MediaType.Video)
                 {
-                    if (!new VideoResolver(_options).IsVideoFile(path))
+                    if (!VideoResolver.IsVideoFile(path, _options))
                     {
                         continue;
                     }

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

+ 27 - 33
Emby.Naming/Video/VideoResolver.cs

@@ -9,38 +9,28 @@ 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))
             {
@@ -67,10 +58,10 @@ namespace Emby.Naming.Video
                 var extension = Path.GetExtension(path.AsSpan());
 
                 // Check supported extensions
-                if (!_options.VideoFileExtensions.Contains(extension, StringComparison.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,10 +72,9 @@ 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 = Path.GetFileNameWithoutExtension(path);
 
@@ -92,12 +82,12 @@ namespace Emby.Naming.Video
 
             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();
                 }
@@ -121,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.AsSpan());
-            return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
+            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.AsSpan());
-            return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
+            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 />

+ 2 - 0
Emby.Server.Implementations/Collections/CollectionManager.cs

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

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

+ 2 - 0
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

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

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

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

@@ -1,9 +1,9 @@
+#nullable disable
 #pragma warning disable CS1591
 
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using SQLitePCL.pretty;
 
@@ -65,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());
         }
@@ -86,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();
 
@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Data
                 DateTimeStyles.None).ToUniversalTime();
         }
 
-        public static bool TryReadDateTime(this IReadOnlyList<IResultSetValue> reader, int index, out DateTime result)
+        public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Data
             return false;
         }
 
-        public static bool TryGetGuid(this IReadOnlyList<IResultSetValue> reader, int index, out Guid result)
+        public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -131,17 +131,17 @@ namespace Emby.Server.Implementations.Data
             return true;
         }
 
-        public static bool IsDbNull(this IResultSetValue result)
+        public static bool IsDbNull(this ResultSetValue result)
         {
             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 TryGetString(this IReadOnlyList<IResultSetValue> reader, int index, out string result)
+        public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
         {
             result = null;
             var item = reader[index];
@@ -154,12 +154,12 @@ namespace Emby.Server.Implementations.Data
             return true;
         }
 
-        public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
+        public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
         {
             return result[index].ToBool();
         }
 
-        public static bool TryGetBoolean(this IReadOnlyList<IResultSetValue> reader, int index, out bool result)
+        public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -172,7 +172,7 @@ namespace Emby.Server.Implementations.Data
             return true;
         }
 
-        public static bool TryGetInt32(this IReadOnlyList<IResultSetValue> reader, int index, out int result)
+        public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -185,12 +185,12 @@ namespace Emby.Server.Implementations.Data
             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 bool TryGetInt64(this IReadOnlyList<IResultSetValue> reader, int index, out long result)
+        public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -203,7 +203,7 @@ namespace Emby.Server.Implementations.Data
             return true;
         }
 
-        public static bool TryGetSingle(this IReadOnlyList<IResultSetValue> reader, int index, out float result)
+        public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.Data
             return true;
         }
 
-        public static bool TryGetDouble(this IReadOnlyList<IResultSetValue> reader, int index, out double result)
+        public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
         {
             var item = reader[index];
             if (item.IsDbNull())
@@ -229,7 +229,7 @@ namespace Emby.Server.Implementations.Data
             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();
         }
@@ -441,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())
             {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 294 - 227
Emby.Server.Implementations/Data/SqliteItemRepository.cs


+ 3 - 1
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

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

+ 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/Dto/DtoService.cs

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

+ 5 - 3
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.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
-    <PackageReference Include="sharpcompress" Version="0.28.2" />
-    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.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)
             {

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

@@ -43,14 +43,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
             return GetSession((HttpContext)requestContext);
         }
 
-        public async Task<User> GetUser(HttpContext requestContext)
+        public async Task<User?> GetUser(HttpContext requestContext)
         {
             var session = await GetSession(requestContext).ConfigureAwait(false);
 
             return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
         }
 
-        public Task<User> GetUser(object requestContext)
+        public Task<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
 {

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

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

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

+ 8 - 11
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)
                 {
@@ -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;

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

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

+ 2 - 0
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

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

+ 2 - 0
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

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

+ 6 - 4
Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs

@@ -1,21 +1,23 @@
-#pragma warning disable CS1591
-
 using System.Collections.Generic;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.LiveTv;
 
 namespace Emby.Server.Implementations.LiveTv
 {
+    /// <summary>
+    /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
+    /// </summary>
     public class LiveTvConfigurationFactory : IConfigurationFactory
     {
+        /// <inheritdoc />
         public IEnumerable<ConfigurationStore> GetConfigurations()
         {
             return new ConfigurationStore[]
             {
                 new ConfigurationStore
                 {
-                     ConfigurationType = typeof(LiveTvOptions),
-                     Key = "livetv"
+                    ConfigurationType = typeof(LiveTvOptions),
+                    Key = "livetv"
                 }
             };
         }

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

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

+ 6 - 4
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1,3 +1,5 @@
+#nullable disable
+
 #pragma warning disable CS1591
 
 using System;
@@ -2264,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             if (dataSourceChanged)
             {
-                _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+                _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
             }
 
             return info;
@@ -2307,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             _config.SaveConfiguration("livetv", config);
 
-            _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 
             return info;
         }
@@ -2319,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv
             config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
 
             _config.SaveConfiguration("livetv", config);
-            _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
         }
 
         public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
@@ -2353,7 +2355,7 @@ namespace Emby.Server.Implementations.LiveTv
             var tunerChannelMappings =
                 tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
 
-            _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+            _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
 
             return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
         }

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

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

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.