aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/label-commenter-config.yml43
-rw-r--r--.github/workflows/automation.yml26
-rw-r--r--.github/workflows/commands.yml119
-rw-r--r--.github/workflows/label-commenter.yml22
-rw-r--r--.github/workflows/merge-conflicts.yml17
-rw-r--r--.github/workflows/rebase.yml27
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs5
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs2
-rw-r--r--Emby.Naming/Video/FlagParser.cs53
-rw-r--r--Emby.Naming/Video/Format3DParser.cs88
-rw-r--r--Emby.Naming/Video/Format3DResult.cs23
-rw-r--r--Emby.Naming/Video/StackResolver.cs4
-rw-r--r--Emby.Naming/Video/VideoFileInfo.cs7
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs233
-rw-r--r--Emby.Naming/Video/VideoResolver.cs60
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs44
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs3
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs507
-rw-r--r--Emby.Server.Implementations/Data/TypeMapper.cs16
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs9
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs4
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs10
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs20
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs2
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs58
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs9
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs17
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs10
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs (renamed from Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs)46
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs20
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs4
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs34
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs5
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs21
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs41
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs40
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs28
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs54
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs3
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs10
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs1
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs67
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs30
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs1
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs1
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs4
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs28
-rw-r--r--Jellyfin.Server.Implementations/Events/EventManager.cs7
-rw-r--r--Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs10
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs32
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs35
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs91
-rw-r--r--Jellyfin.Server/Startup.cs1
-rw-r--r--MediaBrowser.Common/Extensions/StringBuilderExtensions.cs35
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs8
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs6
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs9
-rw-r--r--MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs12
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs9
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs3
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs3
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs74
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs51
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs6
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs5
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildComparer.cs3
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs7
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs3
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs21
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs13
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs11
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs12
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs6
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs7
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerEventInfo.cs3
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs (renamed from MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs)5
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs27
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs1
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs9
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs8
-rw-r--r--MediaBrowser.Controller/Net/WebSocketListenerState.cs17
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs12
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs11
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs2
-rw-r--r--MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs2
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs21
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ItemLookupInfo.cs6
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs18
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs (renamed from MediaBrowser.Controller/Resolvers/BaseItemResolver.cs)2
-rw-r--r--MediaBrowser.Controller/Sorting/AlphanumComparator.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs26
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs9
-rw-r--r--MediaBrowser.Model/Configuration/PathSubstitution.cs2
-rw-r--r--MediaBrowser.Model/Extensions/StringHelper.cs3
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs1
-rw-r--r--MediaBrowser.Model/Plugins/PluginInfo.cs2
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs2
-rw-r--r--MediaBrowser.Model/Tasks/ITaskTrigger.cs4
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs4
-rw-r--r--MediaBrowser.Model/Updates/VersionInfo.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs42
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs18
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs3
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs25
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs7
-rw-r--r--jellyfin.ruleset3
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj2
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj3
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs33
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs15
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj2
-rw-r--r--tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs14
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj3
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs6
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs7
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs9
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs208
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StubTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs242
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs32
-rw-r--r--tests/Jellyfin.Networking.Tests/IPHostTests.cs53
-rw-r--r--tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs49
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj3
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs60
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs49
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj3
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs31
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs53
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs48
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs31
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs4
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs4
169 files changed, 2218 insertions, 1551 deletions
diff --git a/.github/label-commenter-config.yml b/.github/label-commenter-config.yml
deleted file mode 100644
index 78b75be43..000000000
--- a/.github/label-commenter-config.yml
+++ /dev/null
@@ -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.
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index de1590c74..8da2349c8 100644
--- a/.github/workflows/automation.yml
+++ b/.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
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
new file mode 100644
index 000000000..e0b91ecee
--- /dev/null
+++ b/.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
diff --git a/.github/workflows/label-commenter.yml b/.github/workflows/label-commenter.yml
deleted file mode 100644
index be9216cc1..000000000
--- a/.github/workflows/label-commenter.yml
+++ /dev/null
@@ -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
diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml
deleted file mode 100644
index 1b04eab46..000000000
--- a/.github/workflows/merge-conflicts.yml
+++ /dev/null
@@ -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 }}
diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml
deleted file mode 100644
index 3eec9fa03..000000000
--- a/.github/workflows/rebase.yml
+++ /dev/null
@@ -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 }}
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index c63aec64e..5e952e47b 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/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);
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index f9d06c09b..1fade985b 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/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;
}
diff --git a/Emby.Naming/Video/FlagParser.cs b/Emby.Naming/Video/FlagParser.cs
deleted file mode 100644
index 439de1813..000000000
--- a/Emby.Naming/Video/FlagParser.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Emby.Naming/Video/Format3DParser.cs b/Emby.Naming/Video/Format3DParser.cs
index 4fd5d78ba..089089989 100644
--- a/Emby.Naming/Video/Format3DParser.cs
+++ b/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;
}
}
}
diff --git a/Emby.Naming/Video/Format3DResult.cs b/Emby.Naming/Video/Format3DResult.cs
index ac935f203..aac959c13 100644
--- a/Emby.Naming/Video/Format3DResult.cs
+++ b/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; }
}
}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 550c42961..36f65a562 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/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();
diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs
index 1457db737..481773ff6 100644
--- a/Emby.Naming/Video/VideoFileInfo.cs
+++ b/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()
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 7b6a1705b..7da2dcd7a 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/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;
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 27e73208c..c4ac5fdc6 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/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);
}
}
}
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 8c919db43..d38535634 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -25,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>();
@@ -34,11 +39,6 @@ namespace Emby.Server.Implementations.AppBase
private bool _configurationLoaded;
/// <summary>
- /// The _configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
-
- /// <summary>
/// The _configuration.
/// </summary>
private BaseApplicationConfiguration _configuration;
@@ -299,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)
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index de770f59e..0308a68e4 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -33,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);
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 2d060dd65..3ae28fadf 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -43,6 +43,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
{
+ private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
private readonly IServerConfigurationManager _config;
@@ -1045,18 +1046,34 @@ namespace Emby.Server.Implementations.Data
return Array.Empty<ItemImageInfo>();
}
- var list = new List<ItemImageInfo>();
- foreach (var part in value.SpanSplit('|'))
+ // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
+ var valueSpan = value.AsSpan();
+ var count = valueSpan.CountOccurrences('|') + 1;
+
+ var position = 0;
+ var result = new ItemImageInfo[count];
+ foreach (var part in valueSpan.Split('|'))
{
var image = ItemImageInfoFromValueString(part);
if (image != null)
{
- list.Add(image);
+ result[position++] = image;
}
}
- return list.ToArray();
+ if (position == count)
+ {
+ return result;
+ }
+
+ if (position == 0)
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+ return result[..position];
}
private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
@@ -2250,10 +2267,8 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
}
- private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns)
+ private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
- var list = startColumns.ToList();
-
foreach (var field in _allFields)
{
if (!HasField(query, field))
@@ -2261,28 +2276,28 @@ namespace Emby.Server.Implementations.Data
switch (field)
{
case ItemFields.Settings:
- list.Remove("IsLocked");
- list.Remove("PreferredMetadataCountryCode");
- list.Remove("PreferredMetadataLanguage");
- list.Remove("LockedFields");
+ columns.Remove("IsLocked");
+ columns.Remove("PreferredMetadataCountryCode");
+ columns.Remove("PreferredMetadataLanguage");
+ columns.Remove("LockedFields");
break;
case ItemFields.ServiceName:
- list.Remove("ExternalServiceId");
+ columns.Remove("ExternalServiceId");
break;
case ItemFields.SortName:
- list.Remove("ForcedSortName");
+ columns.Remove("ForcedSortName");
break;
case ItemFields.Taglines:
- list.Remove("Tagline");
+ columns.Remove("Tagline");
break;
case ItemFields.Tags:
- list.Remove("Tags");
+ columns.Remove("Tags");
break;
case ItemFields.IsHD:
// do nothing
break;
default:
- list.Remove(field.ToString());
+ columns.Remove(field.ToString());
break;
}
}
@@ -2290,60 +2305,60 @@ namespace Emby.Server.Implementations.Data
if (!HasProgramAttributes(query))
{
- list.Remove("IsMovie");
- list.Remove("IsSeries");
- list.Remove("EpisodeTitle");
- list.Remove("IsRepeat");
- list.Remove("ShowId");
+ columns.Remove("IsMovie");
+ columns.Remove("IsSeries");
+ columns.Remove("EpisodeTitle");
+ columns.Remove("IsRepeat");
+ columns.Remove("ShowId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!HasStartDate(query))
{
- list.Remove("StartDate");
+ columns.Remove("StartDate");
}
if (!HasTrailerTypes(query))
{
- list.Remove("TrailerTypes");
+ columns.Remove("TrailerTypes");
}
if (!HasArtistFields(query))
{
- list.Remove("AlbumArtists");
- list.Remove("Artists");
+ columns.Remove("AlbumArtists");
+ columns.Remove("Artists");
}
if (!HasSeriesFields(query))
{
- list.Remove("SeriesId");
+ columns.Remove("SeriesId");
}
if (!HasEpisodeAttributes(query))
{
- list.Remove("SeasonName");
- list.Remove("SeasonId");
+ columns.Remove("SeasonName");
+ columns.Remove("SeasonId");
}
if (!query.DtoOptions.EnableImages)
{
- list.Remove("Images");
+ columns.Remove("Images");
}
if (EnableJoinUserData(query))
{
- list.Add("UserDatas.UserId");
- list.Add("UserDatas.lastPlayedDate");
- list.Add("UserDatas.playbackPositionTicks");
- list.Add("UserDatas.playcount");
- list.Add("UserDatas.isFavorite");
- list.Add("UserDatas.played");
- list.Add("UserDatas.rating");
+ columns.Add("UserDatas.UserId");
+ columns.Add("UserDatas.lastPlayedDate");
+ columns.Add("UserDatas.playbackPositionTicks");
+ columns.Add("UserDatas.playcount");
+ columns.Add("UserDatas.isFavorite");
+ columns.Add("UserDatas.played");
+ columns.Add("UserDatas.rating");
}
if (query.SimilarTo != null)
@@ -2391,7 +2406,7 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SimilarityScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
@@ -2418,10 +2433,8 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SearchScore");
- list.Add(builder.ToString());
+ columns.Add(builder.ToString());
}
-
- return list;
}
private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@@ -2487,31 +2500,25 @@ namespace Emby.Server.Implementations.Data
private string GetGroupBy(InternalItemsQuery query)
{
- var groups = new List<string>();
-
- if (EnableGroupByPresentationUniqueKey(query))
+ var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
+ if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
{
- groups.Add("PresentationUniqueKey");
+ return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
}
- if (query.GroupBySeriesPresentationUniqueKey)
+ if (enableGroupByPresentationUniqueKey)
{
- groups.Add("SeriesPresentationUniqueKey");
+ return " Group by PresentationUniqueKey";
}
- if (groups.Count > 0)
+ if (query.GroupBySeriesPresentationUniqueKey)
{
- return " Group by " + string.Join(',', groups);
+ return " Group by SeriesPresentationUniqueKey";
}
return string.Empty;
}
- private string GetFromText(string alias = "A")
- {
- return " from TypedBaseItems " + alias;
- }
-
public int GetCount(InternalItemsQuery query)
{
if (query == null)
@@ -2529,17 +2536,21 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List<string> { "count(distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
+ var commandText = commandTextBuilder.ToString();
int count;
using (var connection = GetConnection(true))
{
@@ -2581,20 +2592,23 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 1024)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2602,15 +2616,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (var connection = GetConnection(true))
{
@@ -2766,20 +2783,27 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = _retriveItemColumns.ToList();
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 512)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
var whereText = whereClauses.Count == 0 ?
string.Empty :
- " where " + string.Join(" AND ", whereClauses);
+ string.Join(" AND ", whereClauses);
- commandText += whereText
- + GetGroupBy(query)
- + GetOrderByText(query);
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -2787,43 +2811,58 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List<string>();
+ var itemQuery = string.Empty;
+ var totalRecordCountQuery = string.Empty;
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ itemQuery = commandTextBuilder.ToString();
}
if (query.EnableTotalRecordCount)
{
- commandText = string.Empty;
+ commandTextBuilder.Clear();
+
+ commandTextBuilder.Append(" select ");
+ List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (guid)" };
}
- commandText += GetJoinUserDataText(query)
- + whereText;
- statementTexts.Add(commandText);
+ SetFinalColumnsToSelect(query, columnsToSelect);
+
+ commandTextBuilder.AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
+ if (!string.IsNullOrEmpty(whereText))
+ {
+ commandTextBuilder.Append(" where ")
+ .Append(whereText);
+ }
+
+ totalRecordCountQuery = commandTextBuilder.ToString();
}
var list = new List<BaseItem>();
@@ -2833,11 +2872,12 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
+ var itemQueryStatement = PrepareStatement(db, itemQuery);
+ var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = itemQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2867,11 +2907,14 @@ namespace Emby.Server.Implementations.Data
}
}
}
+
+ LogQueryTime("GetItems.ItemQuery", itemQuery, now);
}
+ now = DateTime.UtcNow;
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Length - 1])
+ using (var statement = totalRecordCountQueryStatement)
{
if (EnableJoinUserData(query))
{
@@ -2886,11 +2929,12 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
+
+ LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
}
}, ReadTransactionMode);
}
- LogQueryTime("GetItems", commandText, now);
result.Items = list;
return result;
}
@@ -3023,19 +3067,22 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
- + GetJoinUserDataText(query);
+ var columns = new List<string> { "guid" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandTextBuilder = new StringBuilder("select ", 256)
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandTextBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses);
}
- commandText += GetGroupBy(query)
- + GetOrderByText(query);
+ commandTextBuilder.Append(GetGroupBy(query))
+ .Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@@ -3043,15 +3090,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ commandTextBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
+ var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (var connection = GetConnection(true))
{
@@ -3090,7 +3140,9 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
- var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
+ var columns = new List<string> { "guid", "path" };
+ SetFinalColumnsToSelect(query, columns);
+ var commandText = "select " + string.Join(',', columns) + FromText;
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
@@ -3166,9 +3218,11 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
+ var columns = new List<string> { "guid" };
+ SetFinalColumnsToSelect(query, columns);
var commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
- + GetFromText()
+ + string.Join(',', columns)
+ + FromText
+ GetJoinUserDataText(query);
var whereClauses = GetWhereClauses(query, null);
@@ -3208,19 +3262,23 @@ namespace Emby.Server.Implementations.Data
{
commandText = string.Empty;
+ List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
- commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
+ columnsToSelect = new List<string> { "count (guid)" };
}
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ commandText += " select " + string.Join(',', columnsToSelect) + FromText;
+
commandText += GetJoinUserDataText(query)
+ whereText;
statementTexts.Add(commandText);
@@ -4415,56 +4473,50 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
}
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
var queryTopParentIds = query.TopParentIds;
- if (queryTopParentIds.Length == 1)
+ if (queryTopParentIds.Length > 0)
{
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
+ var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
+
+ if (queryTopParentIds.Length == 1)
{
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- if (statement != null)
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
+ }
+ else
+ {
+ whereClauses.Add("(TopParentId=@TopParentId)");
}
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
- if (statement != null)
- {
- statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
+ statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
}
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
+ else if (queryTopParentIds.Length > 1)
{
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- if (statement != null)
+ var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
+
+ if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
- statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
+ statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
+ }
+ else if (enableItemsByName && includedItemByNameTypes.Count > 1)
+ {
+ var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
+ whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
+ }
+ else
+ {
+ whereClauses.Add("TopParentId in (" + val + ")");
}
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
}
}
@@ -4746,17 +4798,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- var types = new[]
- {
- nameof(Episode),
- nameof(Video),
- nameof(Movie),
- nameof(MusicVideo),
- nameof(Series),
- nameof(Season)
- };
-
- if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
+ || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
{
return true;
}
@@ -5200,37 +5247,45 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
-
- var commandText = "Select Value From ItemValues where " + typeClause;
+ var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
+ if (itemValueTypes.Length == 1)
+ {
+ stringBuilder.Append('=')
+ .Append(itemValueTypes[0]);
+ }
+ else
+ {
+ stringBuilder.Append(" in (")
+ .AppendJoin(',', itemValueTypes)
+ .Append(')');
+ }
if (withItemTypes.Count > 0)
{
- var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', withItemTypes)
+ .Append("))");
}
if (excludeItemTypes.Count > 0)
{
- var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'"));
- commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
+ stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
+ .AppendJoinInSingleQuotes(',', excludeItemTypes)
+ .Append("))");
}
- commandText += " Group By CleanValue";
+ stringBuilder.Append(" Group By CleanValue");
+ var commandText = stringBuilder.ToString();
var list = new List<string>();
using (var connection = GetConnection(true))
+ using (var statement = PrepareStatement(connection, commandText))
{
- using (var statement = PrepareStatement(connection, commandText))
+ foreach (var row in statement.ExecuteQuery())
{
- foreach (var row in statement.ExecuteQuery())
+ if (row.TryGetString(0, out var result))
{
- if (row.TryGetString(0, out var result))
- {
- list.Add(result);
- }
+ list.Add(result);
}
}
}
@@ -5256,18 +5311,19 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
- ("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
+ ("Type=" + itemValueTypes[0]) :
+ ("Type in (" + string.Join(',', itemValueTypes) + ")");
InternalItemsQuery typeSubQuery = null;
- Dictionary<string, string> itemCountColumns = null;
+ string itemCountColumns = null;
+ var stringBuilder = new StringBuilder(1024);
var typesToCount = query.IncludeItemTypes;
if (typesToCount.Length > 0)
{
- var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B");
+ stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
typeSubQuery = new InternalItemsQuery(query.User)
{
@@ -5283,20 +5339,22 @@ AND Type = @InternalPersonType)");
};
var whereClauses = GetWhereClauses(typeSubQuery, null);
- whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", whereClauses)
+ .Append(" AND ")
+ .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
+ .Append(typeClause)
+ .Append(")) as itemTypes");
- itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
-
- itemCountColumns = new Dictionary<string, string>()
- {
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
- };
+ itemCountColumns = stringBuilder.ToString();
+ stringBuilder.Clear();
}
List<string> columns = _retriveItemColumns.ToList();
- if (itemCountColumns != null)
+ // Unfortunately we need to add it to columns to ensure the order of the columns in the select
+ if (!string.IsNullOrEmpty(itemCountColumns))
{
- columns.AddRange(itemCountColumns.Values);
+ columns.Add(itemCountColumns);
}
// do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
@@ -5317,20 +5375,20 @@ AND Type = @InternalPersonType)");
IsSeries = query.IsSeries
};
- columns = GetFinalColumnsToSelect(query, columns);
-
- var commandText = "select "
- + string.Join(',', columns)
- + GetFromText()
- + GetJoinUserDataText(query);
+ SetFinalColumnsToSelect(query, columns);
var innerWhereClauses = GetWhereClauses(innerQuery, null);
- var innerWhereText = innerWhereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", innerWhereClauses);
+ stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
+ .Append(typeClause)
+ .Append(" AND ItemId in (select guid from TypedBaseItems");
+ if (innerWhereClauses.Count > 0)
+ {
+ stringBuilder.Append(" where ")
+ .AppendJoin(" AND ", innerWhereClauses);
+ }
- var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))";
+ stringBuilder.Append("))");
var outerQuery = new InternalItemsQuery(query.User)
{
@@ -5355,23 +5413,31 @@ AND Type = @InternalPersonType)");
};
var outerWhereClauses = GetWhereClauses(outerQuery, null);
-
if (outerWhereClauses.Count != 0)
{
- whereText += " AND " + string.Join(" AND ", outerWhereClauses);
+ stringBuilder.Append(" AND ")
+ .AppendJoin(" AND ", outerWhereClauses);
}
- commandText += whereText + " group by PresentationUniqueKey";
+ var whereText = stringBuilder.ToString();
+ stringBuilder.Clear();
+
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columns)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText)
+ .Append(" group by PresentationUniqueKey");
if (query.OrderBy.Count != 0
|| query.SimilarTo != null
|| !string.IsNullOrEmpty(query.SearchTerm))
{
- commandText += GetOrderByText(query);
+ stringBuilder.Append(GetOrderByText(query));
}
else
{
- commandText += " order by SortName";
+ stringBuilder.Append(" order by SortName");
}
if (query.Limit.HasValue || query.StartIndex.HasValue)
@@ -5380,32 +5446,39 @@ AND Type = @InternalPersonType)");
if (query.Limit.HasValue || offset > 0)
{
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" LIMIT ")
+ .Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
+ stringBuilder.Append(" OFFSET ")
+ .Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
- var statementTexts = new List<string>();
+ string commandText = string.Empty;
+
if (!isReturningZeroItems)
{
- statementTexts.Add(commandText);
+ commandText = stringBuilder.ToString();
}
+ string countText = string.Empty;
if (query.EnableTotalRecordCount)
{
- var countText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
+ stringBuilder.Clear();
+ var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
+ SetFinalColumnsToSelect(query, columnsToSelect);
+ stringBuilder.Append("select ")
+ .AppendJoin(',', columnsToSelect)
+ .Append(FromText)
+ .Append(GetJoinUserDataText(query))
+ .Append(whereText);
- statementTexts.Add(countText);
+ countText = stringBuilder.ToString();
}
var list = new List<(BaseItem, ItemCounts)>();
@@ -5415,11 +5488,9 @@ AND Type = @InternalPersonType)");
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts);
-
if (!isReturningZeroItems)
{
- using (var statement = statements[0])
+ using (var statement = PrepareStatement(db, commandText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5460,13 +5531,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
- commandText = "select "
- + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
- + GetFromText()
- + GetJoinUserDataText(query)
- + whereText;
-
- using (var statement = statements[statements.Length - 1])
+ using (var statement = PrepareStatement(db, countText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
diff --git a/Emby.Server.Implementations/Data/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs
index 7f1306d15..064664e1f 100644
--- a/Emby.Server.Implementations/Data/TypeMapper.cs
+++ b/Emby.Server.Implementations/Data/TypeMapper.cs
@@ -28,19 +28,9 @@ namespace Emby.Server.Implementations.Data
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));
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 57e040338..556662585 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -31,8 +31,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.6" />
<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="3.0.1" />
+ <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>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index cc3e4a2c2..0a4efd73c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -108,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()
@@ -120,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)
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 211941f44..2e72b18f5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -54,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)
{
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 332fb3385..d3bcd5e13 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -28,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)
{
@@ -44,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)
{
@@ -66,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;
@@ -89,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private void UpdateTimerCallback(object state)
+ private void UpdateTimerCallback(object? state)
{
lock (_syncLock)
{
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 6a554e68a..64d802457 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/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;
@@ -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
{
@@ -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)
@@ -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);
});
}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 3380e29d4..c7d113963 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/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;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index f8d8197d4..028673529 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -696,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);
}
- }).Where(i => i != null);
+
+ if (result != null)
+ {
+ yield return result;
+ }
+ }
}
/// <summary>
@@ -1065,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>())
@@ -1095,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);
@@ -2076,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)
@@ -2101,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();
}
@@ -2112,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();
@@ -2500,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 />
@@ -2679,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
return changed;
}
+ /// <inheritdoc />
public NamingOptions GetNamingOptions()
{
if (_namingOptions == null)
@@ -2692,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
};
}
@@ -2712,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));
@@ -2758,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));
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 38e81d14c..b812b6b61 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -352,7 +352,7 @@ namespace Emby.Server.Implementations.Library
private string[] NormalizeLanguage(string language)
{
- if (language == null)
+ if (string.IsNullOrEmpty(language))
{
return Array.Empty<string>();
}
@@ -381,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
@@ -411,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);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index a3dcdc944..cdb492022 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -47,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;
@@ -66,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)
{
@@ -84,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)
{
@@ -102,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)
{
@@ -132,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)
{
@@ -252,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);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 02c528764..97f96f746 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -6,6 +6,7 @@ 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;
@@ -257,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
{
@@ -537,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)
{
@@ -547,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);
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index 108863869..32245f899 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
index ba916af38..098f193fb 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
+++ b/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"
}
};
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 1f1628900..d964769b5 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -2266,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv
if (dataSourceChanged)
{
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
return info;
@@ -2309,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv
_config.SaveConfiguration("livetv", config);
- _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
@@ -2321,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)
@@ -2355,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));
}
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
index 582b64923..15df0dcf1 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.LiveTv;
@@ -10,34 +9,55 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.LiveTv
{
- public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
+ /// <summary>
+ /// The "Refresh Guide" scheduled task.
+ /// </summary>
+ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILiveTvManager _liveTvManager;
private readonly IConfigurationManager _config;
- public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
+ /// </summary>
+ /// <param name="liveTvManager">The live tv manager.</param>
+ /// <param name="config">The configuration manager.</param>
+ public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
_config = config;
}
+ /// <inheritdoc />
public string Name => "Refresh Guide";
+ /// <inheritdoc />
public string Description => "Downloads channel information from live tv services.";
+ /// <inheritdoc />
public string Category => "Live TV";
- public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
+ /// <inheritdoc />
+ public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public string Key => "RefreshGuide";
+
+ /// <inheritdoc />
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var manager = (LiveTvManager)_liveTvManager;
return manager.RefreshChannels(progress, cancellationToken);
}
- /// <summary>
- /// Creates the triggers that define when the task will run.
- /// </summary>
- /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ /// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -51,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
-
- public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
-
- public string Key => "RefreshGuide";
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 00a37bb02..5941613cf 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -40,6 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public virtual bool IsSupported => true;
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public abstract string Type { get; }
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index c5700db71..54de841fe 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -583,7 +583,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
@@ -624,7 +623,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
- _networkManager,
_streamHelper);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 50a2d9abb..58e0c7448 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -12,7 +12,6 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -30,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IServerApplicationHost _appHost;
private readonly IHdHomerunChannelCommands _channelCommands;
private readonly int _numTuners;
- private readonly INetworkManager _networkManager;
public HdHomerunUdpStream(
MediaSourceInfo mediaSource,
@@ -42,12 +40,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
- INetworkManager networkManager,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_appHost = appHost;
- _networkManager = networkManager;
OriginalStreamId = originalStreamId;
_channelCommands = channelCommands;
_numTuners = numTuners;
@@ -128,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using (udpClient)
using (hdHomerunManager)
{
- if (!(ex is OperationCanceledException))
+ if (ex is not OperationCanceledException)
{
Logger.LogError(ex, "Error opening live stream:");
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 69035dac9..8fa6f5ad6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -29,6 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
+ private static readonly string[] _disallowedSharedStreamExtensions =
+ {
+ ".mkv",
+ ".mp4",
+ ".m3u8",
+ ".mpd"
+ };
+
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
@@ -67,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClientFactory, _appHost)
+ return await new M3uParser(Logger, _httpClientFactory)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -88,14 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions =
- {
- ".mkv",
- ".mp4",
- ".m3u8",
- ".mpd"
- };
-
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 48a0c3cd3..40a162890 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -21,15 +21,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3uParser
{
+ private const string ExtInfPrefix = "#EXTINF:";
+
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerApplicationHost _appHost;
- public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
+ public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
}
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
@@ -61,8 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return File.OpenRead(info.Url);
}
- private const string ExtInfPrefix = "#EXTINF:";
-
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
{
var channels = new List<ChannelInfo>();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 137ed27e2..f572151b8 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -91,8 +91,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var taskCompletionSource = new TaskCompletionSource<bool>();
- var now = DateTime.UtcNow;
-
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
// OpenedMediaSource.Protocol = MediaProtocol.File;
@@ -120,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
- throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
+ throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index dd5dee1d1..b1ff28c2c 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -5,7 +5,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
@@ -169,12 +168,22 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public CultureDto FindLanguageInfo(string language)
- => GetCultures()
- .FirstOrDefault(i =>
- string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase)
- || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase)
- || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase)
- || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
+ {
+ // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
+ for (var i = 0; i < _cultures.Count; i++)
+ {
+ var culture = _cultures[i];
+ if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
+ || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
+ || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
+ {
+ return culture;
+ }
+ }
+
+ return default;
+ }
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
@@ -224,7 +233,7 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(rating));
}
- if (_unratedValues.Contains(rating, StringComparer.OrdinalIgnoreCase))
+ if (_unratedValues.Contains(rating.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -252,11 +261,11 @@ namespace Emby.Server.Implementations.Localization
var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
- rating = rating.Substring(index).TrimStart(':').Trim();
+ var trimmedRating = rating.AsSpan(index).TrimStart(':').Trim();
- if (!string.IsNullOrWhiteSpace(rating))
+ if (!trimmedRating.IsEmpty)
{
- return GetRatingLevel(rating);
+ return GetRatingLevel(trimmedRating.ToString());
}
}
@@ -318,7 +327,8 @@ namespace Emby.Server.Implementations.Localization
return _dictionaries.GetOrAdd(
culture,
- f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
+ this);
}
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 48281b75f..8fd61f2bc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -394,7 +394,7 @@ namespace Emby.Server.Implementations.Plugins
Category = packageInfo.Category,
Changelog = versionInfo.Changelog ?? string.Empty,
Description = packageInfo.Description,
- Id = new Guid(packageInfo.Id),
+ Id = packageInfo.Id,
Name = packageInfo.Name,
Overview = packageInfo.Overview,
Owner = packageInfo.Owner,
@@ -455,7 +455,8 @@ namespace Emby.Server.Implementations.Plugins
try
{
_logger.LogDebug("Creating instance of {Type}", type);
- var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
+ // _appHost.ServiceProvider is already assigned when we create the plugins
+ var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider!, type);
if (plugin == null)
{
// Create a dummy record for the providers.
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index ccbd4289e..d7e320754 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -711,11 +711,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
}
- return new DailyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- TaskOptions = options
- };
+ return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
@@ -730,12 +726,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
}
- return new WeeklyTrigger
- {
- TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
- DayOfWeek = info.DayOfWeek.Value,
- TaskOptions = options
- };
+ return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
}
if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
@@ -745,16 +736,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
}
- return new IntervalTrigger
- {
- Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
- TaskOptions = options
- };
+ return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{
- return new StartupTrigger();
+ return new StartupTrigger(options);
}
throw new ArgumentException("Unrecognized trigger type: " + info.Type);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index 3b63536a4..29ab6a73d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Threading;
using MediaBrowser.Model.Tasks;
@@ -10,29 +8,31 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that fires everyday.
/// </summary>
- public class DailyTrigger : ITaskTrigger
+ public sealed class DailyTrigger : ITaskTrigger
{
- /// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
+ private readonly TimeSpan _timeOfDay;
+ private Timer? _timer;
/// <summary>
- /// Gets or sets the time of day to trigger the task to run.
+ /// Initializes a new instance of the <see cref="DailyTrigger"/> class.
/// </summary>
- /// <value>The time of day.</value>
- public TimeSpan TimeOfDay { get; set; }
+ /// <param name="timeofDay">The time of day to trigger the task to run.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public DailyTrigger(TimeSpan timeofDay, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -47,14 +47,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
- var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
- triggerDate = triggerDate.Add(TimeOfDay);
+ var triggerDate = now.TimeOfDay > _timeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(_timeOfDay);
var dueTime = triggerDate - now;
logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -70,10 +70,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index e13782fe0..30568e809 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Linq;
using System.Threading;
@@ -11,31 +9,32 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that runs repeatedly on an interval.
/// </summary>
- public class IntervalTrigger : ITaskTrigger
+ public sealed class IntervalTrigger : ITaskTrigger
{
+ private readonly TimeSpan _interval;
private DateTime _lastStartDate;
+ private Timer? _timer;
/// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
- /// Gets or sets the interval.
+ /// Initializes a new instance of the <see cref="IntervalTrigger"/> class.
/// </summary>
- /// <value>The interval.</value>
- public TimeSpan Interval { get; set; }
+ /// <param name="interval">The interval.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public IntervalTrigger(TimeSpan interval, TaskOptions taskOptions)
+ {
+ _interval = interval;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -57,7 +56,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
else
{
- triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval);
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(_interval);
}
if (DateTime.UtcNow > triggerDate)
@@ -73,7 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
dueTime = maxDueTime;
}
- Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -89,10 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
index ced14195b..18b9a8b75 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -12,24 +10,28 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Class StartupTaskTrigger.
/// </summary>
- public class StartupTrigger : ITaskTrigger
+ public sealed class StartupTrigger : ITaskTrigger
{
+ public const int DelayMs = 3000;
+
/// <summary>
- /// Occurs when [triggered].
+ /// Initializes a new instance of the <see cref="StartupTrigger"/> class.
/// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- public int DelayMs { get; set; }
+ /// <param name="taskOptions">The options of this task.</param>
+ public StartupTrigger(TaskOptions taskOptions)
+ {
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
- public StartupTrigger()
- {
- DelayMs = 3000;
- }
+ /// <summary>
+ /// Gets the options of this task.
+ /// </summary>
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index a67f940b7..36ae190b0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Threading;
using MediaBrowser.Model.Tasks;
@@ -10,35 +8,34 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Represents a task trigger that fires on a weekly basis.
/// </summary>
- public class WeeklyTrigger : ITaskTrigger
+ public sealed class WeeklyTrigger : ITaskTrigger
{
- /// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
- /// Gets or sets the time of day to trigger the task to run.
- /// </summary>
- /// <value>The time of day.</value>
- public TimeSpan TimeOfDay { get; set; }
+ private readonly TimeSpan _timeOfDay;
+ private readonly DayOfWeek _dayOfWeek;
+ private Timer? _timer;
/// <summary>
- /// Gets or sets the day of week.
+ /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class.
/// </summary>
- /// <value>The day of week.</value>
- public DayOfWeek DayOfWeek { get; set; }
+ /// <param name="timeofDay">The time of day to trigger the task to run.</param>
+ /// <param name="dayOfWeek">The day of week.</param>
+ /// <param name="taskOptions">The options of this task.</param>
+ public WeeklyTrigger(TimeSpan timeofDay, DayOfWeek dayOfWeek, TaskOptions taskOptions)
+ {
+ _timeOfDay = timeofDay;
+ _dayOfWeek = dayOfWeek;
+ TaskOptions = taskOptions;
+ }
/// <summary>
- /// Gets or sets the options of this task.
+ /// Occurs when [triggered].
/// </summary>
- public TaskOptions TaskOptions { get; set; }
+ public event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the timer.
+ /// Gets the options of this task.
/// </summary>
- /// <value>The timer.</value>
- private Timer Timer { get; set; }
+ public TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
@@ -53,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var triggerDate = GetNextTriggerDateTime();
- Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ _timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
@@ -65,22 +62,22 @@ namespace Emby.Server.Implementations.ScheduledTasks
var now = DateTime.Now;
// If it's on the same day
- if (now.DayOfWeek == DayOfWeek)
+ if (now.DayOfWeek == _dayOfWeek)
{
// It's either later today, or a week from now
- return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay);
+ return now.TimeOfDay < _timeOfDay ? now.Date.Add(_timeOfDay) : now.Date.AddDays(7).Add(_timeOfDay);
}
var triggerDate = now.Date;
// Walk the date forward until we get to the trigger day
- while (triggerDate.DayOfWeek != DayOfWeek)
+ while (triggerDate.DayOfWeek != _dayOfWeek)
{
triggerDate = triggerDate.AddDays(1);
}
// Return the trigger date plus the time offset
- return triggerDate.Add(TimeOfDay);
+ return triggerDate.Add(_timeOfDay);
}
/// <summary>
@@ -96,10 +93,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
private void DisposeTimer()
{
- if (Timer != null)
- {
- Timer.Dispose();
- }
+ _timer?.Dispose();
}
/// <summary>
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 8d8b82f0a..5ff73de81 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -21,7 +21,8 @@ namespace Emby.Server.Implementations.Serialization
private static XmlSerializer GetSerializer(Type type)
=> _serializers.GetOrAdd(
type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
- _ => new XmlSerializer(type));
+ (_, t) => new XmlSerializer(t),
+ type);
/// <summary>
/// Serializes to writer.
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index ac589b03c..6cf9a8f71 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -25,6 +25,10 @@ namespace Emby.Server.Implementations
cacheDirectoryPath,
webDirectoryPath)
{
+ // ProgramDataPath cannot change when the server is running, so cache these to avoid allocations.
+ RootFolderPath = Path.Join(ProgramDataPath, "root");
+ DefaultUserViewsPath = Path.Combine(RootFolderPath, "default");
+ DefaultInternalMetadataPath = Path.Combine(ProgramDataPath, "metadata");
InternalMetadataPath = DefaultInternalMetadataPath;
}
@@ -32,13 +36,13 @@ namespace Emby.Server.Implementations
/// Gets the path to the base root media directory.
/// </summary>
/// <value>The root folder path.</value>
- public string RootFolderPath => Path.Combine(ProgramDataPath, "root");
+ public string RootFolderPath { get; }
/// <summary>
/// Gets the path to the default user view directory. Used if no specific user view is defined.
/// </summary>
/// <value>The default user views path.</value>
- public string DefaultUserViewsPath => Path.Combine(RootFolderPath, "default");
+ public string DefaultUserViewsPath { get; }
/// <summary>
/// Gets the path to the People directory.
@@ -98,7 +102,7 @@ namespace Emby.Server.Implementations
public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users");
/// <inheritdoc/>
- public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata");
+ public string DefaultInternalMetadataPath { get; }
/// <inheritdoc />
public string InternalMetadataPath { get; set; }
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index ed1dfca59..9fa92a53a 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,5 +1,4 @@
#pragma warning disable CS1591
-#pragma warning disable SA1600
using System;
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 750f00168..8179e26c5 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Net;
using System.Net.Sockets;
@@ -20,17 +18,17 @@ namespace Emby.Server.Implementations.Udp
public sealed class UdpServer : IDisposable
{
/// <summary>
+ /// Address Override Configuration Key.
+ /// </summary>
+ public const string AddressOverrideConfigKey = "PublishedServerUrl";
+
+ /// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
- /// <summary>
- /// Address Override Configuration Key.
- /// </summary>
- public const string AddressOverrideConfigKey = "PublishedServerUrl";
-
private Socket _udpSocket;
private IPEndPoint _endpoint;
private readonly byte[] _receiveBuffer = new byte[8192];
@@ -40,49 +38,58 @@ namespace Emby.Server.Implementations.Udp
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
/// </summary>
- public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration)
+ /// <param name="logger">The logger.</param>
+ /// <param name="appHost">The application host.</param>
+ /// <param name="configuration">The configuration manager.</param>
+ /// <param name="port">The port.</param>
+ public UdpServer(
+ ILogger logger,
+ IServerApplicationHost appHost,
+ IConfiguration configuration,
+ int port)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
+
+ _endpoint = new IPEndPoint(IPAddress.Any, port);
+
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
- ? _config[AddressOverrideConfigKey]
- : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ string? localUrl = _config[AddressOverrideConfigKey];
+ if (string.IsNullOrEmpty(localUrl))
+ {
+ localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
+ }
- if (!string.IsNullOrEmpty(localUrl))
+ if (string.IsNullOrEmpty(localUrl))
{
- var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+ _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ return;
+ }
- try
- {
- await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
- }
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Error sending response message");
- }
+ var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName);
+
+ try
+ {
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
}
- else
+ catch (SocketException ex)
{
- _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined.");
+ _logger.LogError(ex, "Error sending response message");
}
}
/// <summary>
/// Starts the specified port.
/// </summary>
- /// <param name="port">The port.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
- public void Start(int port, CancellationToken cancellationToken)
+ public void Start(CancellationToken cancellationToken)
{
- _endpoint = new IPEndPoint(IPAddress.Any, port);
-
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
- _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
_udpSocket.Bind(_endpoint);
_ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
@@ -90,9 +97,9 @@ namespace Emby.Server.Implementations.Udp
private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
+ var infiniteTask = Task.Delay(-1, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
- var infiniteTask = Task.Delay(-1, cancellationToken);
try
{
var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 2351b7d8c..b0921cbd8 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,7 +1,3 @@
-#nullable disable
-
-#nullable enable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -103,12 +99,12 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
+ public async Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
- List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
+ PackageInfo[]? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetFromJsonAsync<PackageInfo[]>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null)
{
@@ -181,20 +177,14 @@ namespace Emby.Server.Implementations.Updates
// Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
- if (!Guid.TryParse(package.Id, out var packageGuid))
- {
- // Package doesn't have a valid GUID, skip.
- continue;
- }
-
- var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
+ var existing = FilterPackages(result, package.Name, package.Id).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{
var version = package.Versions[i];
- var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
+ var plugin = _pluginManager.GetPlugin(package.Id, version.VersionNumber);
if (plugin != null)
{
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
@@ -233,7 +223,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null)
{
if (name != null)
@@ -241,9 +231,9 @@ namespace Emby.Server.Implementations.Updates
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- if (id != Guid.Empty)
+ if (id != default)
{
- availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
+ availablePackages = availablePackages.Where(x => x.Id == id);
}
if (specificVersion != null)
@@ -258,7 +248,7 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null)
{
@@ -288,7 +278,7 @@ namespace Emby.Server.Implementations.Updates
yield return new InstallationInfo
{
Changelog = v.Changelog,
- Id = new Guid(package.Id),
+ Id = package.Id,
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.SourceUrl,
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 555062e55..62283d038 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1190,7 +1190,8 @@ namespace Jellyfin.Api.Controllers
throw new ArgumentException("StartTimeTicks is not allowed.");
}
- using var cancellationTokenSource = new CancellationTokenSource();
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState(
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 308334b23..6a720b1a4 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -265,6 +265,7 @@ namespace Jellyfin.Api.Controllers
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
};
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index e544d001e..dc64a0f1b 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -373,6 +373,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
{
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index cf35ee23a..264131905 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -97,6 +97,8 @@ namespace Jellyfin.Api.Helpers
}
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
+
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
using var state = await StreamingHelpers.GetStreamingState(
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index fcada0e77..dc5d6715b 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -106,6 +106,7 @@ namespace Jellyfin.Api.Helpers
bool enableAdaptiveBitrateStreaming)
{
var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
return await GetMasterPlaylistInternal(
streamingRequest,
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index b4db0c83e..c295af7eb 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Helpers
{
_activeTranscodingJobs.Remove(job);
- if (!job.CancellationTokenSource!.IsCancellationRequested)
+ if (job.CancellationTokenSource?.IsCancellationRequested == false)
{
job.CancellationTokenSource.Cancel();
}
@@ -751,7 +751,7 @@ namespace Jellyfin.Api.Helpers
_logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
}
- process.Dispose();
+ job.Dispose();
}
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index 9edc19bb6..291e571dc 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -11,7 +11,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// <summary>
/// Class TranscodingJob.
/// </summary>
- public class TranscodingJobDto
+ public class TranscodingJobDto : IDisposable
{
/// <summary>
/// The process lock.
@@ -249,5 +249,31 @@ namespace Jellyfin.Api.Models.PlaybackDtos
}
}
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose all resources.
+ /// </summary>
+ /// <param name="disposing">Whether to dispose all resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ Process?.Dispose();
+ Process = null;
+ KillTimer?.Dispose();
+ KillTimer = null;
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+ TranscodingThrottler?.Dispose();
+ TranscodingThrottler = null;
+ }
+ }
}
}
diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs
index 707002442..c5e66112d 100644
--- a/Jellyfin.Server.Implementations/Events/EventManager.cs
+++ b/Jellyfin.Server.Implementations/Events/EventManager.cs
@@ -43,7 +43,12 @@ namespace Jellyfin.Server.Implementations.Events
private async Task PublishInternal<T>(T eventArgs)
where T : EventArgs
{
- using var scope = _appHost.ServiceProvider.CreateScope();
+ using var scope = _appHost.ServiceProvider?.CreateScope();
+ if (scope == null)
+ {
+ return;
+ }
+
foreach (var service in scope.ServiceProvider.GetServices<IEventConsumer<T>>())
{
try
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 88e2b4152..e29167747 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -79,6 +79,16 @@ namespace Jellyfin.Server.Extensions
}
/// <summary>
+ /// Enables url decoding before binding to the application pipeline.
+ /// </summary>
+ /// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
+ /// <returns>The updated application builder.</returns>
+ public static IApplicationBuilder UseQueryStringDecoding(this IApplicationBuilder appBuilder)
+ {
+ return appBuilder.UseMiddleware<QueryStringDecodingMiddleware>();
+ }
+
+ /// <summary>
/// Adds base url redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index c23da2fd6..2eef223e5 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -45,15 +45,33 @@ namespace Jellyfin.Server.Middleware
var localPath = httpContext.Request.Path.ToString();
var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
- if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
- || string.IsNullOrEmpty(localPath)
- || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(baseUrlPrefix))
{
- // Always redirect back to the default path if the base prefix is invalid or missing
+ var startsWithBaseUrl = localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase);
+
+ if (!startsWithBaseUrl
+ && (localPath.Equals("/health", StringComparison.OrdinalIgnoreCase)
+ || localPath.Equals("/health/", StringComparison.OrdinalIgnoreCase)))
+ {
+ _logger.LogDebug("Redirecting /health check");
+ httpContext.Response.Redirect(baseUrlPrefix + "/health");
+ return;
+ }
+
+ if (!startsWithBaseUrl)
+ {
+ // Always redirect back to the default path if the base prefix is invalid or missing
+ _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
+ httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ return;
+ }
+ }
+ else if (string.IsNullOrEmpty(localPath)
+ || localPath.Equals("/", StringComparison.Ordinal))
+ {
+ // Always redirect back to the default path if root is requested.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
+ httpContext.Response.Redirect("/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
return;
}
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
new file mode 100644
index 000000000..fd0ebbf43
--- /dev/null
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// URL decodes the querystring before binding.
+ /// </summary>
+ public class QueryStringDecodingMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
+ /// </summary>
+ /// <param name="next">The next delegate in the pipeline.</param>
+ public QueryStringDecodingMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ /// <summary>
+ /// Executes the middleware action.
+ /// </summary>
+ /// <param name="httpContext">The current HTTP context.</param>
+ /// <returns>The async task.</returns>
+ public async Task Invoke(HttpContext httpContext)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+
+ await _next(httpContext).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
new file mode 100644
index 000000000..310a3d31a
--- /dev/null
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using MediaBrowser.Common.Extensions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Jellyfin.Server.Middleware
+{
+ /// <summary>
+ /// Defines the <see cref="UrlDecodeQueryFeature"/>.
+ /// </summary>
+ public class UrlDecodeQueryFeature : IQueryFeature
+ {
+ private IQueryCollection? _store;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class.
+ /// </summary>
+ /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param>
+ public UrlDecodeQueryFeature(IQueryFeature feature)
+ {
+ Query = feature.Query;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>.
+ /// </summary>
+ public IQueryCollection Query
+ {
+ get
+ {
+ return _store ?? QueryCollection.Empty;
+ }
+
+ set
+ {
+ // Only interested in where the querystring is encoded which shows up as one key with nothing in the value.
+ if (value.Count != 1)
+ {
+ _store = value;
+ return;
+ }
+
+ // Encoded querystrings have no value, so don't process anything if a value is present.
+ var (key, stringValues) = value.First();
+ if (!string.IsNullOrEmpty(stringValues))
+ {
+ _store = value;
+ return;
+ }
+
+ // Unencode and re-parse querystring.
+ var unencodedKey = HttpUtility.UrlDecode(key);
+
+ if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ {
+ // Don't do anything if it's not encoded.
+ _store = value;
+ return;
+ }
+
+ var pairs = new Dictionary<string, StringValues>();
+ var queryString = unencodedKey.SpanSplit('&');
+
+ foreach (var pair in queryString)
+ {
+ var i = pair.IndexOf('=');
+ if (i == -1)
+ {
+ // encoded is an equals.
+ // We use TryAdd so duplicate keys get ignored
+ pairs.TryAdd(pair.ToString(), StringValues.Empty);
+ continue;
+ }
+
+ var k = pair[..i].ToString();
+ var v = pair[(i + 1)..].ToString();
+ if (!pairs.TryAdd(k, new StringValues(v)))
+ {
+ pairs[k] = StringValues.Concat(pairs[k], v);
+ }
+ }
+
+ _store = new QueryCollection(pairs);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index f75139884..60cdc2f6f 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -160,6 +160,7 @@ namespace Jellyfin.Server
mainApp.UseAuthentication();
mainApp.UseJellyfinApiSwagger(_serverConfigurationManager);
+ mainApp.UseQueryStringDecoding();
mainApp.UseRouting();
mainApp.UseAuthorization();
diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
new file mode 100644
index 000000000..75d654f23
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Extension methods for the <see cref="StringBuilder"/> class.
+ /// </summary>
+ public static class StringBuilderExtensions
+ {
+ /// <summary>
+ /// Concatenates and appends the members of a collection in single quotes using the specified delimiter.
+ /// </summary>
+ /// <param name="builder">The string builder.</param>
+ /// <param name="delimiter">The character delimiter.</param>
+ /// <param name="values">The collection of strings to concatenate.</param>
+ /// <returns>The updated string builder.</returns>
+ public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values)
+ {
+ var len = values.Count;
+ for (var i = 0; i < len; i++)
+ {
+ builder.Append('\'')
+ .Append(values[i])
+ .Append('\'')
+ .Append(delimiter);
+ }
+
+ // remove last ,
+ builder.Length--;
+
+ return builder;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 46d93e494..192a77611 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Reflection;
@@ -12,7 +10,7 @@ namespace MediaBrowser.Common
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
- public delegate object CreationDelegateFactory(Type type);
+ public delegate object? CreationDelegateFactory(Type type);
/// <summary>
/// An interface to be implemented by the applications hosting a kernel.
@@ -22,7 +20,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
- event EventHandler HasPendingRestartChanged;
+ event EventHandler? HasPendingRestartChanged;
/// <summary>
/// Gets the name.
@@ -63,7 +61,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Gets or sets the service provider.
/// </summary>
- IServiceProvider ServiceProvider { get; set; }
+ IServiceProvider? ServiceProvider { get; set; }
/// <summary>
/// Gets the application version.
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
index c2a28e0a2..458494bdc 100644
--- a/MediaBrowser.Common/Updates/IInstallationManager.cs
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Updates
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
- Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
+ Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all available packages that are supported by this version.
@@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Updates
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? specificVersion = null);
/// <summary>
@@ -60,7 +60,7 @@ namespace MediaBrowser.Common.Updates
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
- Guid? id = default,
+ Guid id = default,
Version? minVersion = null,
Version? specificVersion = null);
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index 68119cfed..ffc274c5d 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -52,7 +53,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -83,7 +84,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
- return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -93,7 +94,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
- return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
diff --git a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
index a2dc5682d..0539b9048 100644
--- a/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
+++ b/MediaBrowser.Controller/Channels/IDisableMediaSourceDisplay.cs
@@ -1,8 +1,12 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.Channels
+namespace MediaBrowser.Controller.Channels
{
+ /// <summary>
+ /// Disable media source display.
+ /// </summary>
+ /// <remarks>
+ /// <see cref="Channel"/> can inherit this interface to disable being displayed.
+ /// </remarks>
public interface IDisableMediaSourceDisplay
{
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
index 2682de51c..bc7683125 100644
--- a/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
+++ b/MediaBrowser.Controller/Channels/ISupportsMediaProbe.cs
@@ -1,8 +1,9 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.Channels
+namespace MediaBrowser.Controller.Channels
{
+ /// <summary>
+ /// Channel supports media probe.
+ /// </summary>
public interface ISupportsMediaProbe
{
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 800f7a8bb..4e640d421 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 9bfead8b3..c7f61a90b 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
index 204175ed5..9ef92bc98 100644
--- a/MediaBrowser.Controller/Drawing/ImageHelper.cs
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -1,7 +1,4 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
using MediaBrowser.Model.Drawing;
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 533130fc8..fe1bc62ab 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -155,11 +155,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
@@ -185,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <exception cref="ArgumentNullException">The id is empty.</exception>
public BaseItem FindVirtualChild(Guid id)
{
if (id.Equals(Guid.Empty))
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index 4c2b7cb7c..576ab67a2 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -82,19 +83,19 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
- var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
+ var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty;
if (ParentIndexNumber.HasValue)
{
- songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
+ songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey;
}
songKey += Name;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 0928a8073..c0cd81110 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -94,7 +94,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.IsSaveLocalMetadataEnabled();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (IsAccessedByName)
{
@@ -102,7 +102,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return Task.CompletedTask;
}
- return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+ return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken);
}
public override List<string> GetUserDataKeys()
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 6e46b4cec..6137ddbf7 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -620,7 +620,11 @@ namespace MediaBrowser.Controller.Entities
public string ForcedSortName
{
get => _forcedSortName;
- set { _forcedSortName = value; _sortName = null; }
+ set
+ {
+ _forcedSortName = value;
+ _sortName = null;
+ }
}
private string _sortName;
@@ -666,14 +670,12 @@ namespace MediaBrowser.Controller.Entities
{
if (SourceType == SourceType.Channel)
{
- return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
+ return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
}
ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
- basePath = System.IO.Path.Combine(basePath, "library");
-
- return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString);
+ return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString);
}
/// <summary>
@@ -1258,7 +1260,7 @@ namespace MediaBrowser.Controller.Entities
// Support plex/xbmc convention
files.AddRange(fileSystemChildren
- .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
+ .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
.OfType<Audio.Audio>()
@@ -1319,14 +1321,16 @@ namespace MediaBrowser.Controller.Entities
{
var extras = new List<Video>();
- var folders = fileSystemChildren.Where(i => i.IsDirectory).ToArray();
+ var libraryOptions = new LibraryOptions();
+ var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList();
foreach (var extraFolderName in AllExtrasTypesFolderNames)
{
var files = folders
.Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => FileSystem.GetFiles(i.FullName));
- extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+ // Re-using the same instance of LibraryOptions since it looks like it's never being altered.
+ extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions)
.OfType<Video>()
.Select(item =>
{
@@ -1337,7 +1341,7 @@ namespace MediaBrowser.Controller.Entities
}
// Use some hackery to get the extra type based on foldername
- item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty), true, out ExtraType extraType)
+ item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType)
? extraType
: Model.Entities.ExtraType.Unknown;
@@ -1427,10 +1431,10 @@ namespace MediaBrowser.Controller.Entities
/// Refreshes owned items such as trailers, theme videos, special features, etc.
/// Returns true or false indicating if changes were found.
/// </summary>
- /// <param name="options"></param>
- /// <param name="fileSystemChildren"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="options">The metadata refresh options.</param>
+ /// <param name="fileSystemChildren">The list of filesystem children.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var themeSongsChanged = false;
@@ -1772,7 +1776,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="user">The user.</param>
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
+ /// <exception cref="ArgumentNullException">If user is null.</exception>
public bool IsParentalAllowed(User user)
{
if (user == null)
@@ -1917,7 +1921,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="user">The user.</param>
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
public virtual bool IsVisible(User user)
{
if (user == null)
@@ -2215,7 +2219,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="type">The type.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ /// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
public bool HasImage(ImageType type, int imageIndex)
{
return GetImageInfo(type, imageIndex) != null;
@@ -2323,7 +2327,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.IsLocalFile)
.Select(i => System.IO.Path.GetDirectoryName(i.Path))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .SelectMany(directoryService.GetFilePaths)
+ .SelectMany(path => directoryService.GetFilePaths(path))
.ToList();
var deletedImages = ImageInfos
@@ -2344,9 +2348,8 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>System.String.</returns>
- /// <exception cref="InvalidOperationException">
- /// </exception>
- /// <exception cref="ArgumentNullException">item</exception>
+ /// <exception cref="InvalidOperationException"> </exception>
+ /// <exception cref="ArgumentNullException">Item is null.</exception>
public string GetImagePath(ImageType imageType, int imageIndex)
=> GetImageInfo(imageType, imageIndex)?.Path;
@@ -2433,7 +2436,15 @@ namespace MediaBrowser.Controller.Entities
throw new ArgumentException("No image info for chapter images");
}
- return ImageInfos.Where(i => i.Type == imageType);
+ // Yield return is more performant than LINQ Where on an Array
+ for (var i = 0; i < ImageInfos.Length; i++)
+ {
+ var imageInfo = ImageInfos[i];
+ if (imageInfo.Type == imageType)
+ {
+ yield return imageInfo;
+ }
+ }
}
/// <summary>
@@ -2442,7 +2453,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
- /// <exception cref="ArgumentException">Cannot call AddImages with chapter images</exception>
+ /// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
{
if (imageType == ImageType.Chapter)
@@ -2465,7 +2476,7 @@ namespace MediaBrowser.Controller.Entities
}
var existing = existingImages
- .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
+ .Find(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
@@ -2496,8 +2507,7 @@ namespace MediaBrowser.Controller.Entities
var newImagePaths = images.Select(i => i.FullName).ToList();
var deleted = existingImages
- .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
- .ToList();
+ .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path));
if (deleted.Count > 0)
{
@@ -2526,10 +2536,11 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the file system path to delete when the item is to be deleted.
/// </summary>
- /// <returns></returns>
+ /// <returns>The metadata for the deleted paths.</returns>
public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
@@ -2636,6 +2647,7 @@ namespace MediaBrowser.Controller.Entities
MetadataCountryCode = GetPreferredMetadataCountryCode(),
MetadataLanguage = GetPreferredMetadataLanguage(),
Name = GetNameForMetadataLookup(),
+ OriginalTitle = OriginalTitle,
ProviderIds = ProviderIds,
IndexNumber = IndexNumber,
ParentIndexNumber = ParentIndexNumber,
@@ -2889,7 +2901,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Updates the official rating based on content and returns true or false indicating if it changed.
/// </summary>
- /// <returns></returns>
+ /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
public bool UpdateRatingToItems(IList<BaseItem> children)
{
var currentOfficialRating = OfficialRating;
@@ -2905,7 +2917,9 @@ namespace MediaBrowser.Controller.Entities
OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
- return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty,
+ return !string.Equals(
+ currentOfficialRating ?? string.Empty,
+ OfficialRating ?? string.Empty,
StringComparison.OrdinalIgnoreCase);
}
@@ -3002,7 +3016,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public bool Equals(BaseItem item) => Object.Equals(Id, item?.Id);
+ public bool Equals(BaseItem other) => object.Equals(Id, other?.Id);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Id);
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index c39b18891..89ad392a4 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -1,6 +1,3 @@
-#nullable disable
-
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index d0fb3997d..4a721ca44 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -315,16 +315,16 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
- /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 29d837c14..541747422 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -207,8 +207,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Unable to add + item.Name</exception>
+ /// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception>
public void AddChild(BaseItem item, CancellationToken cancellationToken)
{
item.SetParent(this);
@@ -274,20 +273,20 @@ namespace MediaBrowser.Controller.Entities
public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
{
- return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem)));
+ return ValidateChildren(progress, new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken: cancellationToken);
}
/// <summary>
/// Validates that the children of the folder still exist.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="metadataRefreshOptions">The metadata refresh options.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
+ public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, CancellationToken cancellationToken = default)
{
- return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
+ return ValidateChildrenInternal(progress, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
@@ -327,13 +326,13 @@ namespace MediaBrowser.Controller.Entities
/// Validates the children internal.
/// </summary>
/// <param name="progress">The progress.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (recursive)
{
@@ -342,7 +341,7 @@ namespace MediaBrowser.Controller.Entities
try
{
- await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false);
+ await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
finally
{
@@ -353,7 +352,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -575,7 +574,7 @@ namespace MediaBrowser.Controller.Entities
private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
{
return RunTasks(
- (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService),
+ (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, null, directoryService, cancellationToken),
children,
progress,
cancellationToken);
@@ -988,7 +987,13 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- items = GetChildren(user, true).Where(filter);
+ // need to pass this param to the children.
+ var childQuery = new InternalItemsQuery
+ {
+ DisplayAlbumFolders = query.DisplayAlbumFolders
+ };
+
+ items = GetChildren(user, true, childQuery).Where(filter);
}
return PostFilterAndSort(items, query, true);
@@ -1013,7 +1018,7 @@ namespace MediaBrowser.Controller.Entities
if (!string.IsNullOrEmpty(query.NameStartsWith))
{
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
+ items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.CurrentCultureIgnoreCase));
}
if (!string.IsNullOrEmpty(query.NameLessThan))
@@ -1324,10 +1329,23 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query)
{
- foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
+ // If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
+ IEnumerable<BaseItem> children = null;
+ if ((query?.DisplayAlbumFolders ?? false) && (this is MusicAlbum))
+ {
+ children = Children;
+ query = null;
+ }
+
+ // If there are not sub-folders, proceed as normal.
+ if (children == null)
+ {
+ children = GetEligibleChildrenForRecursiveChildren(user);
+ }
+
+ foreach (var child in children)
{
bool? isVisibleToUser = null;
@@ -1596,7 +1614,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Refreshes the linked children.
/// </summary>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
+ /// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
{
if (SupportsShortcutChildren)
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index 698643b44..b80a5be3b 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -107,7 +107,11 @@ namespace MediaBrowser.Controller.Entities
return base.RequiresRefresh();
}
- /// <inheridoc />
+ /// <summary>
+ /// This is called before any metadata refresh and returns true if changes were made.
+ /// </summary>
+ /// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
+ /// <returns>true if the item has change, else false.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
index b027a0cb1..ae01c223e 100644
--- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs
+++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
@@ -1,7 +1,7 @@
namespace MediaBrowser.Controller.Entities
{
/// <summary>
- /// Interface IHasScreenshots.
+ /// The item has screenshots.
/// </summary>
public interface IHasScreenshots
{
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 75fea365b..ebaf5506d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -265,6 +265,11 @@ namespace MediaBrowser.Controller.Entities
public bool? IsDeadPerson { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether album sub-folders should be returned if they exist.
+ /// </summary>
+ public bool? DisplayAlbumFolders { get; set; }
+
public InternalItemsQuery()
{
AlbumArtistIds = Array.Empty<Guid>();
diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
index 66fc44b8a..4e58e2942 100644
--- a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
+using System;
using System.Collections.Generic;
using MediaBrowser.Model.IO;
@@ -28,7 +29,7 @@ namespace MediaBrowser.Controller.Entities
public int GetHashCode(LinkedChild obj)
{
- return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode();
+ return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal);
}
}
} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 1b4cc7a78..31c179bca 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -218,8 +218,8 @@ namespace MediaBrowser.Controller.Entities.TV
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "")
- + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty)
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
}
/// <summary>
@@ -287,7 +287,8 @@ namespace MediaBrowser.Controller.Entities.TV
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = Path,
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 5e2053dcc..aa62bb35b 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -122,7 +122,7 @@ namespace MediaBrowser.Controller.Entities.TV
var series = Series;
if (series != null)
{
- return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
+ return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
}
}
@@ -135,7 +135,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <returns>System.String.</returns>
protected override string CreateSortName()
{
- return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name;
+ return IndexNumber != null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name;
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 2dea2e50b..2b15a52f0 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -109,11 +109,11 @@ namespace MediaBrowser.Controller.Entities
return base.GetNonCachedChildren(directoryService);
}
- protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
ClearCache();
- await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, refreshOptions, directoryService, cancellationToken)
.ConfigureAwait(false);
ClearCache();
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index 1e6c01bf8..57dc9b59b 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -172,7 +172,7 @@ namespace MediaBrowser.Controller.Entities
return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 15a4573c2..add734f62 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -55,12 +55,12 @@ namespace MediaBrowser.Controller.Entities
// if (query.IncludeItemTypes != null &&
// query.IncludeItemTypes.Length == 1 &&
// string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
- //{
+ // {
// if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
// {
// return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
// }
- //}
+ // }
switch (viewType)
{
@@ -344,12 +344,14 @@ namespace MediaBrowser.Controller.Entities
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
var result = _tvSeriesManager.GetNextUp(
- new NextUpQuery
- {
- Limit = query.Limit,
- StartIndex = query.StartIndex,
- UserId = query.User.Id
- }, parentFolders, query.DtoOptions);
+ new NextUpQuery
+ {
+ Limit = query.Limit,
+ StartIndex = query.StartIndex,
+ UserId = query.User.Id
+ },
+ parentFolders,
+ query.DtoOptions);
return result;
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 723027a88..d05b5df2f 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -482,7 +482,8 @@ namespace MediaBrowser.Controller.Entities
{
if (!IsInMixedFolder)
{
- return new[] {
+ return new[]
+ {
new FileSystemMetadata
{
FullName = ContainingFolderPath,
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
index 1853896ee..291485e75 100644
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs
@@ -22,6 +22,27 @@ namespace MediaBrowser.Controller.Extensions
return Normalize(string.Concat(chars), NormalizationForm.FormC);
}
+ /// <summary>
+ /// Counts the number of occurrences of [needle] in the string.
+ /// </summary>
+ /// <param name="value">The haystack to search in.</param>
+ /// <param name="needle">The character to search for.</param>
+ /// <returns>The number of occurrences of the [needle] character.</returns>
+ public static int CountOccurrences(this ReadOnlySpan<char> value, char needle)
+ {
+ var count = 0;
+ var length = value.Length;
+ for (var i = 0; i < length; i++)
+ {
+ if (value[i] == needle)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true)
{
if (stripStringOnFailure)
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index 3db60ae0b..b8a0bf331 100644
--- a/MediaBrowser.Controller/IO/FileData.cs
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.IO
/// <param name="flattenFolderDepth">The flatten folder depth.</param>
/// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param>
/// <returns>Dictionary{System.StringFileSystemInfo}.</returns>
- /// <exception cref="ArgumentNullException">path</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path" /> is <c>null</c> or empty.</exception>
public static FileSystemMetadata[] GetFilteredFileSystemEntries(
IDirectoryService directoryService,
string path,
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 3fd4ff899..7a4ba6a24 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -352,6 +353,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
/// <param name="uniqueId">The unique identifier.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
string name,
Guid parentId,
@@ -365,10 +367,11 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The shadow view.</returns>
UserView GetShadowView(
BaseItem parent,
- string viewType,
- string sortName);
+ string viewType,
+ string sortName);
/// <summary>
/// Determines whether [is video file] [the specified path].
@@ -593,5 +596,11 @@ namespace MediaBrowser.Controller.Library
BaseItem GetParentItem(string parentId, Guid? userId);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
+
+ /// <summary>
+ /// Gets or creates a static instance of <see cref="NamingOptions"/>.
+ /// </summary>
+ /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns>
+ NamingOptions GetNamingOptions();
}
}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index 58499e853..e5dcfcff0 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -49,17 +49,16 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Get all user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>The user item data.</returns>
List<UserItemData> GetAllUserData(Guid userId);
/// <summary>
/// Save the all provided user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The array of user data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index c95b0ea32..1801b1c41 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -61,16 +61,16 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="newName">The new name.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
Task RenameUser(User user, string newName);
/// <summary>
/// Updates the user.
/// </summary>
/// <param name="user">The user.</param>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
void UpdateUser(User user);
/// <summary>
@@ -87,8 +87,8 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="name">The name of the new user.</param>
/// <returns>The created user.</returns>
- /// <exception cref="ArgumentNullException">name</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception>
+ /// <exception cref="ArgumentException"><paramref name="name"/> already exists.</exception>
Task<User> CreateUserAsync(string name);
/// <summary>
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 0e2d8fb02..521e37274 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -39,7 +39,7 @@ namespace MediaBrowser.Controller.Library
public IDirectoryService DirectoryService { get; }
/// <summary>
- /// Gets the file system children.
+ /// Gets or sets the file system children.
/// </summary>
/// <value>The file system children.</value>
public FileSystemMetadata[] FileSystemChildren { get; set; }
@@ -242,14 +242,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
public override int GetHashCode()
{
- return Path.GetHashCode();
+ return Path.GetHashCode(StringComparison.Ordinal);
}
/// <summary>
/// Equals the specified args.
/// </summary>
/// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <returns><c>true</c> if the arguments are the same, <c>false</c> otherwise.</returns>
protected bool Equals(ItemResolveArgs args)
{
if (args != null)
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index e2adec000..9d638a0bf 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -53,6 +53,7 @@ namespace MediaBrowser.Controller.LiveTv
}
private static string EmbyServiceName = "Emby";
+
public override double GetDefaultPrimaryImageAspectRatio()
{
var serviceName = ServiceName;
@@ -150,14 +151,14 @@ namespace MediaBrowser.Controller.LiveTv
[JsonIgnore]
public override string ContainingFolderPath => Path;
- //[JsonIgnore]
+ // [JsonIgnore]
// public override string MediaType
- //{
+ // {
// get
// {
// return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio;
// }
- //}
+ // }
[JsonIgnore]
public bool IsAiring
diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
index 728387c56..92eb0be9c 100644
--- a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
@@ -1,6 +1,3 @@
-#nullable disable
-
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 3f3a505ea..ee76ff080 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -21,8 +21,9 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
- <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
+ <ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
+ <ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 61f3bc771..745ee6bdb 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -1,4 +1,4 @@
-#nullable disable
+#nullable disable
#pragma warning disable CS1591
@@ -8,7 +8,6 @@ using MediaBrowser.Model.Dlna;
namespace MediaBrowser.Controller.MediaEncoding
{
- // For now until api and media encoding layers are unified
public class BaseEncodingJobOptions
{
/// <summary>
@@ -202,4 +201,4 @@ namespace MediaBrowser.Controller.MediaEncoding
StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
-}
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index feb5883e5..26b0bc3de 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1166,7 +1166,9 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/
- param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ param += string.Format(
+ CultureInfo.InvariantCulture,
+ " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
profileScore.ToString(_usCulture),
crf,
qmin,
@@ -1296,7 +1298,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hevc_qsv use -level 51 instead of -level 153.
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
{
- param += " -level " + hevcLevel / 3;
+ param += " -level " + (hevcLevel / 3);
}
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
@@ -1392,7 +1394,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedProfile = requestedProfiles[0];
// strip spaces because they may be stripped out on the query string as well
if (!string.IsNullOrEmpty(videoStream.Profile)
- && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", "", StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
+ && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Profile);
var requestedScore = GetVideoProfileScore(requestedProfile);
@@ -1801,7 +1803,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isTranscodingAudio
&& state.TranscodingType != TranscodingJobType.Progressive
&& resultChannels.HasValue
- && (resultChannels.Value > 2 && resultChannels.Value < 6 || resultChannels.Value == 7))
+ && ((resultChannels.Value > 2 && resultChannels.Value < 6) || resultChannels.Value == 7))
{
resultChannels = 2;
}
@@ -2129,8 +2131,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null);
}
- decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth);
- decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight);
+ decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth, CultureInfo.InvariantCulture);
+ decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight, CultureInfo.InvariantCulture);
decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth;
decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight;
decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth;
@@ -2197,12 +2199,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isTonemappingSupported = IsTonemappingSupported(state, options);
var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
- var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)&& isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
+ var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))
|| (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
-
var outputPixFmt = "format=nv12";
if (isP010PixFmtRequired)
{
@@ -3175,8 +3176,8 @@ namespace MediaBrowser.Controller.MediaEncoding
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
if (state.ReadInputAtNativeFramerate
- || mediaSource.Protocol == MediaProtocol.File
- && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
+ || (mediaSource.Protocol == MediaProtocol.File
+ && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)))
{
state.InputVideoSync = "-1";
state.InputAudioSync = "1";
@@ -3549,7 +3550,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hw decoder name
+ /// Gets a hw decoder name.
/// </summary>
public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10)
{
@@ -3567,7 +3568,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system
+ /// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system.
/// </summary>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10)
{
@@ -3693,7 +3694,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (flags.Count > 0)
{
- return " -fflags " + string.Join("", flags);
+ return " -fflags " + string.Join(string.Empty, flags);
}
return string.Empty;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 30e2ac42f..bc0318ad7 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -69,6 +69,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
private TranscodeReason[] _transcodeReasons = null;
+
public TranscodeReason[] TranscodeReasons
{
get
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index aa5e2c403..b23c95112 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -6,6 +6,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 855467e8e..d8995ce74 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -270,13 +270,4 @@ namespace MediaBrowser.Controller.Net
GC.SuppressFinalize(this);
}
}
-
- public class WebSocketListenerState
- {
- public DateTime DateLastSendUtc { get; set; }
-
- public long InitialDelayMs { get; set; }
-
- public long IntervalMs { get; set; }
- }
}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index e50cd9781..c8c5caf80 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,9 +1,5 @@
-#nullable disable
-
#pragma warning disable CS1591
-#nullable enable
-
using System;
using System.Net;
using System.Net.WebSockets;
@@ -60,11 +56,11 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of websocket message data.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
+ /// <exception cref="ArgumentNullException">The message is null.</exception>
Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
Task ProcessAsync(CancellationToken cancellationToken = default);
diff --git a/MediaBrowser.Controller/Net/WebSocketListenerState.cs b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
new file mode 100644
index 000000000..70604d60a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketListenerState.cs
@@ -0,0 +1,17 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class WebSocketListenerState
+ {
+ public DateTime DateLastSendUtc { get; set; }
+
+ public long InitialDelayMs { get; set; }
+
+ public long IntervalMs { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 56fb36af2..0a9073e7f 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -49,21 +49,23 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Gets chapters for an item.
/// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <returns>The list of chapter info.</returns>
List<ChapterInfo> GetChapters(BaseItem id);
/// <summary>
/// Gets a single chapter for an item.
/// </summary>
- /// <param name="id"></param>
- /// <param name="index"></param>
- /// <returns></returns>
+ /// <param name="id">The item.</param>
+ /// <param name="index">The chapter index.</param>
+ /// <returns>The chapter info at the specified index.</returns>
ChapterInfo GetChapter(BaseItem id, int index);
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="chapters">The list of chapters to save.</param>
void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters);
/// <summary>
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
index 6f5f02123..5fa5834c8 100644
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -40,17 +40,16 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Return all user data associated with the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>The list of user item data.</returns>
List<UserItemData> GetAllUserData(long userId);
/// <summary>
/// Save all user data associated with the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The user item data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index bb9e5da1e..3eaf23515 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -101,7 +101,7 @@ namespace MediaBrowser.Controller.Playlists
return new List<BaseItem>();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
index ea966c282..2b831103a 100644
--- a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
+++ b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
@@ -6,4 +6,4 @@
public interface IRunBeforeStartup
{
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 291a26883..b31270270 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -25,15 +25,16 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- return _cache.GetOrAdd(path, p => _fileSystem.GetFileSystemEntries(p).ToArray());
+ return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
public List<FileSystemMetadata> GetFiles(string path)
{
var list = new List<FileSystemMetadata>();
var items = GetFileSystemEntries(path);
- foreach (var item in items)
+ for (var i = 0; i < items.Length; i++)
{
+ var item = items[i];
if (!item.IsDirectory)
{
list.Add(item);
@@ -48,10 +49,9 @@ namespace MediaBrowser.Controller.Providers
if (!_fileCache.TryGetValue(path, out var result))
{
var file = _fileSystem.GetFileInfo(path);
- var res = file != null && file.Exists ? file : null;
- if (res != null)
+ if (file.Exists)
{
- result = res;
+ result = file;
_fileCache.TryAdd(path, result);
}
}
@@ -62,14 +62,21 @@ namespace MediaBrowser.Controller.Providers
public IReadOnlyList<string> GetFilePaths(string path)
=> GetFilePaths(path, false);
- public IReadOnlyList<string> GetFilePaths(string path, bool clearCache)
+ public IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false)
{
if (clearCache)
{
_filePathCache.TryRemove(path, out _);
}
- return _filePathCache.GetOrAdd(path, p => _fileSystem.GetFilePaths(p).ToList());
+ var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem);
+
+ if (sort)
+ {
+ filePaths.Sort();
+ }
+
+ return filePaths;
}
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 9cee06a4c..b1a36e102 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -15,6 +15,6 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path);
- IReadOnlyList<string> GetFilePaths(string path, bool clearCache);
+ IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
}
}
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
index e6f49c26a..2fd89e3bb 100644
--- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Providers
public string Name { get; set; }
/// <summary>
+ /// Gets or sets the original title
+ /// </summary>
+ /// <value>The original title of the item.</value>
+ public string OriginalTitle { get; set; }
+
+ /// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index 8b0967a6e..7ec1eefcd 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -12,16 +12,26 @@ namespace MediaBrowser.Controller.Providers
{
public class MetadataResult<T>
{
+ // Images aren't always used so the allocation is a waste a lot of the time
+ private List<LocalImageInfo> _images;
+ private List<(string url, ImageType type)> _remoteImages;
+
public MetadataResult()
{
- Images = new List<LocalImageInfo>();
- RemoteImages = new List<(string url, ImageType type)>();
ResultLanguage = "en";
}
- public List<LocalImageInfo> Images { get; set; }
+ public List<LocalImageInfo> Images
+ {
+ get => _images ??= new List<LocalImageInfo>();
+ set => _images = value;
+ }
- public List<(string url, ImageType type)> RemoteImages { get; set; }
+ public List<(string url, ImageType type)> RemoteImages
+ {
+ get => _remoteImages ??= new List<(string url, ImageType type)>();
+ set => _remoteImages = value;
+ }
public List<UserItemData> UserDataList { get; set; }
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index e77593a03..7fd54fcc6 100644
--- a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Resolvers
/// <summary>
/// Class ItemResolver.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of BaseItem.</typeparam>
public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new()
{
diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
index 4d9b98889..e00cadca2 100644
--- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
+++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
@@ -121,7 +121,9 @@ namespace MediaBrowser.Controller.Sorting
return result;
}
}
+#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196
} while (pos1 < len1 && pos2 < len2);
+#pragma warning restore SA1500
return len1 - len2;
}
diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
index bc62ca4d5..fdba64c4a 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
@@ -15,17 +16,6 @@ namespace MediaBrowser.LocalMetadata.Images
/// </summary>
public class EpisodeLocalImageProvider : ILocalImageProvider, IHasOrder
{
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EpisodeLocalImageProvider"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- public EpisodeLocalImageProvider(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
/// <inheritdoc />
public string Name => "Local Images";
@@ -49,14 +39,14 @@ namespace MediaBrowser.LocalMetadata.Images
var parentPathFiles = directoryService.GetFiles(parentPath);
- var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path);
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
}
- private List<LocalImageInfo> GetFilesFromParentFolder(string filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
+ private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
{
- var thumbName = filenameWithoutExtension + "-thumb";
+ var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1);
@@ -67,15 +57,15 @@ namespace MediaBrowser.LocalMetadata.Images
continue;
}
- if (BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
+ if (BaseItem.SupportedImageExtensions.Contains(i.Extension.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
- var currentNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(i);
+ var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.FullName.AsSpan());
- if (string.Equals(filenameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ if (filenameWithoutExtension.Equals(currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
}
- else if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+ else if (currentNameWithoutExtension.Equals(thumbName, StringComparison.OrdinalIgnoreCase))
{
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index cdb778bf2..3af618af8 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -367,7 +367,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource)
{
var prefix = "file";
- if (mediaSource.VideoType == VideoType.BluRay)
+ if (mediaSource.VideoType == VideoType.BluRay
+ || mediaSource.IsoType == IsoType.BluRay)
{
prefix = "bluray";
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
deleted file mode 100644
index cec1aaf08..000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- public static class ParserValues
- {
- public const string NewLine = "\r\n";
- }
-}
diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs
index bffaa8594..2c9b5f005 100644
--- a/MediaBrowser.Model/Configuration/PathSubstitution.cs
+++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace MediaBrowser.Model.Configuration
{
/// <summary>
diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs
index 2d9a6c4db..77cbef00f 100644
--- a/MediaBrowser.Model/Extensions/StringHelper.cs
+++ b/MediaBrowser.Model/Extensions/StringHelper.cs
@@ -17,7 +17,8 @@ namespace MediaBrowser.Model.Extensions
return str;
}
- if (char.IsUpper(str[0]))
+ // We check IsLower instead of IsUpper because both return false for non-letters
+ if (!char.IsLower(str[0]))
{
return str;
}
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
index b3db57b6d..d5c3a6aec 100644
--- a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs
index 25216610d..8eb90bdb0 100644
--- a/MediaBrowser.Model/Plugins/PluginInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
namespace MediaBrowser.Model.Plugins
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
index 85c0aa204..f4d83c28b 100644
--- a/MediaBrowser.Model/Plugins/PluginPageInfo.cs
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace MediaBrowser.Model.Plugins
{
/// <summary>
diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
index db9fba696..999db9605 100644
--- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -14,9 +14,9 @@ namespace MediaBrowser.Model.Tasks
event EventHandler<EventArgs>? Triggered;
/// <summary>
- /// Gets or sets the options of this task.
+ /// Gets the options of this task.
/// </summary>
- TaskOptions TaskOptions { get; set; }
+ TaskOptions TaskOptions { get; }
/// <summary>
/// Stars waiting for the trigger action.
diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs
index 7a82685f0..aeaaa8b35 100644
--- a/MediaBrowser.Model/Updates/PackageInfo.cs
+++ b/MediaBrowser.Model/Updates/PackageInfo.cs
@@ -1,4 +1,3 @@
-#nullable enable
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -16,7 +15,6 @@ namespace MediaBrowser.Model.Updates
public PackageInfo()
{
Versions = Array.Empty<VersionInfo>();
- Id = string.Empty;
Category = string.Empty;
Name = string.Empty;
Overview = string.Empty;
@@ -65,7 +63,7 @@ namespace MediaBrowser.Model.Updates
/// </summary>
/// <value>The name.</value>
[JsonPropertyName("guid")]
- public string Id { get; set; }
+ public Guid Id { get; set; }
/// <summary>
/// Gets or sets the versions.
diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs
index 209092265..03a540dde 100644
--- a/MediaBrowser.Model/Updates/VersionInfo.cs
+++ b/MediaBrowser.Model/Updates/VersionInfo.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Text.Json.Serialization;
using SysVersion = System.Version;
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 4471a25b2..966a3d822 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Manager
/// <summary>
/// Image types that are only one per item.
/// </summary>
- private readonly ImageType[] _singularImages =
+ private static readonly ImageType[] _singularImages =
{
ImageType.Primary,
ImageType.Art,
@@ -208,9 +208,14 @@ namespace MediaBrowser.Providers.Manager
/// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit)
{
- if (_singularImages.Any(i => images.Contains(i) && !HasImage(item, i) && savedOptions.GetLimit(i) > 0))
+ // Using .Any causes the creation of a DisplayClass aka. variable capture
+ for (var i = 0; i < _singularImages.Length; i++)
{
- return false;
+ var type = _singularImages[i];
+ if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0)
+ {
+ return false;
+ }
}
if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit)
@@ -329,7 +334,7 @@ namespace MediaBrowser.Providers.Manager
var deleted = false;
var deletedImages = new List<ItemImageInfo>();
- foreach (var image in item.GetImages(type).ToList())
+ foreach (var image in item.GetImages(type))
{
if (!image.IsLocalFile)
{
@@ -359,9 +364,10 @@ namespace MediaBrowser.Providers.Manager
{
var changed = false;
- foreach (var type in _singularImages)
+ for (var i = 0; i < _singularImages.Length; i++)
{
- var image = images.FirstOrDefault(i => i.Type == type);
+ var type = _singularImages[i];
+ var image = GetFirstLocalImageInfoByType(images, type);
if (image != null)
{
@@ -423,15 +429,29 @@ namespace MediaBrowser.Providers.Manager
return changed;
}
+ private static LocalImageInfo GetFirstLocalImageInfoByType(IReadOnlyList<LocalImageInfo> images, ImageType type)
+ {
+ var len = images.Count;
+ for (var i = 0; i < len; i++)
+ {
+ var image = images[i];
+ if (image.Type == type)
+ {
+ return image;
+ }
+ }
+
+ return null;
+ }
+
private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type)
{
var changed = false;
- var newImages = images.Where(i => i.Type == type).ToList();
-
- var newImageFileInfos = newImages
- .Select(i => i.FileInfo)
- .ToList();
+ var newImageFileInfos = images
+ .FindAll(i => i.Type == type)
+ .Select(i => i.FileInfo)
+ .ToList();
if (item.AddImages(type, newImageFileInfos))
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 02e7e9dbb..827cb69b9 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -28,8 +28,11 @@ namespace MediaBrowser.Providers.Manager
ProviderManager = providerManager;
FileSystem = fileSystem;
LibraryManager = libraryManager;
+ ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
}
+ protected ItemImageProvider ImageProvider { get; }
+
protected IServerConfigurationManager ServerConfigurationManager { get; }
protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
@@ -88,7 +91,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
var localImagesFailed = false;
var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList();
@@ -97,7 +99,7 @@ namespace MediaBrowser.Providers.Manager
try
{
// Always validate images and check for new locally stored ones.
- if (itemImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
+ if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
{
updateType |= ItemUpdateType.ImageUpdate;
}
@@ -143,7 +145,7 @@ namespace MediaBrowser.Providers.Manager
// await FindIdentities(id, cancellationToken).ConfigureAwait(false);
id.IsAutomated = refreshOptions.IsAutomated;
- var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false);
+ var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -160,7 +162,7 @@ namespace MediaBrowser.Providers.Manager
if (providers.Count > 0)
{
- var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
+ var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -577,7 +579,7 @@ namespace MediaBrowser.Providers.Manager
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
{
// Get providers to refresh
- var providers = allImageProviders.Where(i => !(i is ILocalImageProvider)).ToList();
+ var providers = allImageProviders.Where(i => !(i is ILocalImageProvider));
var dateLastImageRefresh = item.DateLastRefreshed;
@@ -589,15 +591,13 @@ namespace MediaBrowser.Providers.Manager
providers = providers
.Where(i =>
{
- var hasFileChangeMonitor = i as IHasItemChangeMonitor;
- if (hasFileChangeMonitor != null)
+ if (i is IHasItemChangeMonitor hasFileChangeMonitor)
{
return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
}
return false;
- })
- .ToList();
+ });
}
return providers;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index dd497845d..2dfaa372c 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1111,7 +1111,7 @@ namespace MediaBrowser.Providers.Manager
await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
break;
case Folder folder:
- await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
+ await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -1122,7 +1122,7 @@ namespace MediaBrowser.Providers.Manager
{
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
+ await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -1144,7 +1144,7 @@ namespace MediaBrowser.Providers.Manager
.Select(i => i.MusicArtist)
.Where(i => i != null);
- var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true));
+ var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken));
await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 36b56f20f..12e1fbea5 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -147,7 +147,8 @@ namespace MediaBrowser.Providers.MediaInfo
{
Path = path,
Protocol = protocol,
- VideoType = item.VideoType
+ VideoType = item.VideoType,
+ IsoType = item.IsoType
}
},
cancellationToken);
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index b086ef07b..3cd7ec772 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -55,38 +54,35 @@ namespace MediaBrowser.Providers.MediaInfo
return streams;
}
- public List<string> GetExternalSubtitleFiles(
+ public IEnumerable<string> GetExternalSubtitleFiles(
Video video,
IDirectoryService directoryService,
bool clearCache)
{
- var list = new List<string>();
-
if (!video.IsFileProtocol)
{
- return list;
+ yield break;
}
var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
foreach (var stream in streams)
{
- list.Add(stream.Path);
+ yield return stream.Path;
}
-
- return list;
}
public void AddExternalSubtitleStreams(
List<MediaStream> streams,
string videoPath,
int startIndex,
- string[] files)
+ IReadOnlyList<string> files)
{
var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
- foreach (var fullName in files)
+ for (var i = 0; i < files.Count; i++)
{
+ var fullName = files[i];
var extension = Path.GetExtension(fullName.AsSpan());
if (!IsSubtitleExtension(extension))
{
@@ -135,15 +131,12 @@ namespace MediaBrowser.Providers.MediaInfo
break;
}
- var language = languageSpan.ToString();
// Try to translate to three character code
// Be flexible and check against both the full and three character versions
+ var language = languageSpan.ToString();
var culture = _localization.FindLanguageInfo(language);
- if (culture != null)
- {
- language = culture.ThreeLetterISOLanguageName;
- }
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
mediaStream = new MediaStream
{
@@ -194,7 +187,7 @@ namespace MediaBrowser.Providers.MediaInfo
IDirectoryService directoryService,
bool clearCache)
{
- var files = directoryService.GetFilePaths(folder, clearCache).OrderBy(i => i).ToArray();
+ var files = directoryService.GetFilePaths(folder, clearCache, true);
AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
}
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index f448ad38b..e49c0e77b 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -42,7 +43,7 @@ namespace Rssdp.Infrastructure
private HttpResponseParser _ResponseParser;
private readonly ILogger _logger;
private ISocketFactory _SocketFactory;
- private readonly INetworkManager _networkManager;
+ private readonly INetworkManager _networkManager;
private int _LocalPort;
private int _MulticastTtl;
@@ -68,7 +69,7 @@ namespace Rssdp.Infrastructure
INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding)
{
-
+
}
/// <summary>
@@ -358,7 +359,7 @@ namespace Rssdp.Infrastructure
{
// Not support IPv6 right now
continue;
- }
+ }
try
{
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 1a9f2bf96..44bc34369 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
+ <!-- disable warning CA1040: Avoid empty interfaces -->
+ <Rule Id="CA1040" Action="Info" />
+
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 839cfb280..1cc67d0a4 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -20,7 +20,7 @@
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 8018b2966..546b2487e 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -15,10 +15,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
index 9ded01f2b..7629d9912 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
@@ -1,34 +1,45 @@
-using System.Text.Json;
+using System.Globalization;
+using System.Text.Json;
+using FsCheck;
+using FsCheck.Xunit;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
- public static class JsonBoolNumberTests
+ public class JsonBoolNumberTests
{
+ private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
+ {
+ Converters =
+ {
+ new JsonBoolNumberConverter()
+ }
+ };
+
[Theory]
[InlineData("1", true)]
[InlineData("0", false)]
[InlineData("2", true)]
[InlineData("true", true)]
[InlineData("false", false)]
- public static void Deserialize_Number_Valid_Success(string input, bool? output)
+ public void Deserialize_Number_Valid_Success(string input, bool? output)
{
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonBoolNumberConverter());
- var value = JsonSerializer.Deserialize<bool>(input, options);
+ var value = JsonSerializer.Deserialize<bool>(input, _jsonOptions);
Assert.Equal(value, output);
}
[Theory]
[InlineData(true, "true")]
[InlineData(false, "false")]
- public static void Serialize_Bool_Success(bool input, string output)
+ public void Serialize_Bool_Success(bool input, string output)
{
- var options = new JsonSerializerOptions();
- options.Converters.Add(new JsonBoolNumberConverter());
- var value = JsonSerializer.Serialize(input, options);
+ var value = JsonSerializer.Serialize(input, _jsonOptions);
Assert.Equal(value, output);
}
+
+ [Property]
+ public Property Deserialize_NonZeroInt_True(NonZeroInt input)
+ => JsonSerializer.Deserialize<bool>(input.ToString(), _jsonOptions).ToProperty();
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
index fd77694b3..2b23c6705 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
@@ -6,14 +6,13 @@ namespace Jellyfin.Common.Tests.Json
{
public class JsonStringConverterTests
{
- private readonly JsonSerializerOptions _jsonSerializerOptions
- = new ()
+ private readonly JsonSerializerOptions _jsonSerializerOptions = new ()
+ {
+ Converters =
{
- Converters =
- {
- new JsonStringConverter()
- }
- };
+ new JsonStringConverter()
+ }
+ };
[Theory]
[InlineData("\"test\"", "test")]
@@ -36,4 +35,4 @@ namespace Jellyfin.Common.Tests.Json
Assert.Equal(deserialized, output);
}
}
-} \ No newline at end of file
+}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index ad1627698..9a8ddafa0 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index f7c21f072..1f6cd541c 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 8321d0255..6b828e113 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
index 5864a0509..0a4e060df 100644
--- a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
+++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
@@ -1,3 +1,6 @@
+using System;
+using FsCheck;
+using FsCheck.Xunit;
using MediaBrowser.Model.Extensions;
using Xunit;
@@ -10,9 +13,20 @@ namespace Jellyfin.Model.Tests.Extensions
[InlineData("banana", "Banana")]
[InlineData("Banana", "Banana")]
[InlineData("ä", "Ä")]
+ [InlineData("\027", "\027")]
public void StringHelper_ValidArgs_Success(string input, string expectedResult)
{
Assert.Equal(expectedResult, StringHelper.FirstToUpper(input));
}
+
+ [Property]
+ public Property FirstToUpper_RandomArg_Correct(NonEmptyString input)
+ {
+ var result = StringHelper.FirstToUpper(input.Item);
+
+ // We check IsLower instead of IsUpper because both return false for non-letters
+ return (!char.IsLower(result[0])).Label("First char is uppercase")
+ .And(input.Item.Length == 1 || result[1..].Equals(input.Item[1..], StringComparison.Ordinal)).Label("Remaining chars are unmodified");
+ }
}
}
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index c5b51ef76..40c51e524 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -10,10 +10,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index ebb134fc3..e386cb8c1 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
index 950899d7e..b1141df47 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs
@@ -58,7 +58,7 @@ namespace Jellyfin.Naming.Tests.Video
{
input = Path.GetFileName(input);
- var result = new VideoResolver(_namingOptions).CleanDateTime(input);
+ var result = VideoResolver.CleanDateTime(input, _namingOptions);
Assert.Equal(expectedName, result.Name, true);
Assert.Equal(expectedYear, result.Year);
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index a720bdade..fb050cf5a 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -7,7 +7,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public sealed class CleanStringTests
{
- private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Theory]
[InlineData("Super movie 480p.mp4", "Super movie")]
@@ -26,7 +26,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
- Assert.True(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName));
+ Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
// TODO: compare spans when XUnit supports it
Assert.Equal(expectedName, newName.ToString());
}
@@ -41,7 +41,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Run lola run (lola rennt) (2009).mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
- Assert.False(_videoResolver.TryCleanString(input, out ReadOnlySpan<char> newName));
+ Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
Assert.True(newName.IsEmpty);
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 2f173b0ce..f872f94f8 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -104,13 +104,6 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(rule, res.Rule);
}
- [Fact]
- public void TestFlagsParser()
- {
- var flags = new FlagParser(_videoOptions).GetFlags(string.Empty);
- Assert.Empty(flags);
- }
-
private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
{
return new ExtraResolver(videoOptions);
diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
index 69de96a47..1762b91b9 100644
--- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs
@@ -22,8 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void Test3DName()
{
- var result =
- new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
+ var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv", _namingOptions);
Assert.Equal("hsbs", result?.Format3D);
Assert.Equal("Oblivion", result?.Name);
@@ -58,15 +57,13 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, bool is3D, string? format3D)
{
- var parser = new Format3DParser(_namingOptions);
-
- var result = parser.Parse(input);
+ var result = Format3DParser.Parse(input, _namingOptions);
Assert.Equal(is3D, result.Is3D);
if (format3D == null)
{
- Assert.Null(result.Format3D);
+ Assert.Null(result?.Format3D);
}
else
{
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 6e803593e..d02f8ae92 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class MultiVersionTests
{
- private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Fact]
public void TestMultiEdition1()
@@ -22,11 +22,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -43,11 +45,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].Extras);
@@ -63,11 +67,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -87,11 +93,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/M/Movie 7.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
@@ -113,11 +121,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie-8.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Mo/Movie 9.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(9, result.Count);
Assert.Empty(result[0].Extras);
@@ -163,11 +175,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Movie/Movie 5.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -188,11 +202,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -214,11 +230,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man[test].mkv",
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -243,11 +261,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man [test].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -266,11 +286,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man - C (2007).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -289,11 +311,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
@@ -314,11 +338,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].Extras);
@@ -334,11 +360,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -354,11 +382,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -374,11 +404,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
@@ -394,11 +426,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -406,7 +440,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestEmptyList()
{
- var result = _videoListResolver.Resolve(new List<FileSystemMetadata>()).ToList();
+ var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList();
Assert.Empty(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
index 6e759c6d6..1d50df7a6 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs
@@ -29,8 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestStubName()
{
- var result =
- new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
+ var result = VideoResolver.ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc", _namingOptions);
Assert.Equal("Oblivion", result?.Name);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 08af76669..9e0776c3c 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoListResolverTests
{
- private readonly VideoListResolver _videoListResolver = new VideoListResolver(new NamingOptions());
+ private readonly NamingOptions _namingOptions = new NamingOptions();
[Fact]
public void TestStackAndExtras()
@@ -40,11 +40,13 @@ namespace Jellyfin.Naming.Tests.Video
"WillyWonka-trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
@@ -67,11 +69,13 @@ namespace Jellyfin.Naming.Tests.Video
"300.nfo"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -85,11 +89,13 @@ namespace Jellyfin.Naming.Tests.Video
"300 trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -103,11 +109,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -122,11 +130,13 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer2.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -140,11 +150,13 @@ namespace Jellyfin.Naming.Tests.Video
"Looper.2012.bluray.720p.x264.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -162,11 +174,13 @@ namespace Jellyfin.Naming.Tests.Video
"My video 5.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(5, result.Count);
}
@@ -180,11 +194,13 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -199,11 +215,13 @@ namespace Jellyfin.Naming.Tests.Video
@"My movie #2.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = true,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -218,11 +236,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012) part1-trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -237,11 +257,13 @@ namespace Jellyfin.Naming.Tests.Video
@"No (2012)-trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -257,11 +279,13 @@ namespace Jellyfin.Naming.Tests.Video
@"trailer.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -277,11 +301,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -294,11 +320,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -311,11 +339,13 @@ namespace Jellyfin.Naming.Tests.Video
@"The Colony.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -329,11 +359,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Sisters and a Wedding - B.avi"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -347,11 +379,13 @@ namespace Jellyfin.Naming.Tests.Video
@"Four Rooms - A.mp4"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(2, result.Count);
}
@@ -365,11 +399,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/movie-trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
@@ -385,11 +421,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Equal(4, result.Count);
}
@@ -403,11 +441,13 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Despicable Me/trailers/trailer.mkv"
};
- var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList()).ToList();
+ var result = VideoListResolver.Resolve(
+ files.Select(i => new FileSystemMetadata
+ {
+ IsDirectory = false,
+ FullName = i
+ }).ToList(),
+ _namingOptions).ToList();
Assert.Single(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index c56046f03..ac5a7a21e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Naming.Tests.Video
{
public class VideoResolverTests
{
- private readonly VideoResolver _videoResolver = new VideoResolver(new NamingOptions());
+ private static NamingOptions _namingOptions = new NamingOptions();
public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
{
@@ -159,27 +159,27 @@ namespace Jellyfin.Naming.Tests.Video
[MemberData(nameof(ResolveFile_ValidFileNameTestData))]
public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
{
- var result = _videoResolver.ResolveFile(expectedResult.Path);
+ var result = VideoResolver.ResolveFile(expectedResult.Path, _namingOptions);
Assert.NotNull(result);
- Assert.Equal(result?.Path, expectedResult.Path);
- Assert.Equal(result?.Container, expectedResult.Container);
- Assert.Equal(result?.Name, expectedResult.Name);
- Assert.Equal(result?.Year, expectedResult.Year);
- Assert.Equal(result?.ExtraType, expectedResult.ExtraType);
- Assert.Equal(result?.Format3D, expectedResult.Format3D);
- Assert.Equal(result?.Is3D, expectedResult.Is3D);
- Assert.Equal(result?.IsStub, expectedResult.IsStub);
- Assert.Equal(result?.StubType, expectedResult.StubType);
- Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
- Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
- Assert.Equal(result?.ToString(), expectedResult.ToString());
+ Assert.Equal(result!.Path, expectedResult.Path);
+ Assert.Equal(result.Container, expectedResult.Container);
+ Assert.Equal(result.Name, expectedResult.Name);
+ Assert.Equal(result.Year, expectedResult.Year);
+ Assert.Equal(result.ExtraType, expectedResult.ExtraType);
+ Assert.Equal(result.Format3D, expectedResult.Format3D);
+ Assert.Equal(result.Is3D, expectedResult.Is3D);
+ Assert.Equal(result.IsStub, expectedResult.IsStub);
+ Assert.Equal(result.StubType, expectedResult.StubType);
+ Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+ Assert.Equal(result.FileNameWithoutExtension.ToString(), expectedResult.FileNameWithoutExtension.ToString());
+ Assert.Equal(result.ToString(), expectedResult.ToString());
}
[Fact]
public void ResolveFile_EmptyPath()
{
- var result = _videoResolver.ResolveFile(string.Empty);
+ var result = VideoResolver.ResolveFile(string.Empty, _namingOptions);
Assert.Null(result);
}
@@ -194,7 +194,7 @@ namespace Jellyfin.Naming.Tests.Video
string.Empty
};
- var results = paths.Select(path => _videoResolver.ResolveDirectory(path)).ToList();
+ var results = paths.Select(path => VideoResolver.ResolveDirectory(path, _namingOptions)).ToList();
Assert.Equal(3, results.Count);
Assert.NotNull(results[0]);
diff --git a/tests/Jellyfin.Networking.Tests/IPHostTests.cs b/tests/Jellyfin.Networking.Tests/IPHostTests.cs
new file mode 100644
index 000000000..ec3a1300c
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/IPHostTests.cs
@@ -0,0 +1,53 @@
+using FsCheck;
+using FsCheck.Xunit;
+using MediaBrowser.Common.Net;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public static class IPHostTests
+ {
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="address">IP Address.</param>
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("127.0.0.1:123")]
+ [InlineData("localhost")]
+ [InlineData("localhost:1345")]
+ [InlineData("www.google.co.uk")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
+ [InlineData("192.168.1.2/255.255.255.0")]
+ [InlineData("192.168.1.2/24")]
+ public static void TryParse_ValidHostStrings_True(string address)
+ => Assert.True(IPHost.TryParse(address, out _));
+
+ [Property]
+ public static Property TryParse_IPv4Address_True(IPv4Address address)
+ => IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ [Property]
+ public static Property TryParse_IPv6Address_True(IPv6Address address)
+ => IPHost.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ /// <summary>
+ /// All should be invalid address strings.
+ /// </summary>
+ /// <param name="address">Invalid address strings.</param>
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
+ public static void TryParse_InvalidAddressString_False(string address)
+ => Assert.False(IPHost.TryParse(address, out _));
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs
new file mode 100644
index 000000000..aa2dbc57a
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/IPNetAddressTests.cs
@@ -0,0 +1,49 @@
+using FsCheck;
+using FsCheck.Xunit;
+using MediaBrowser.Common.Net;
+using Xunit;
+
+namespace Jellyfin.Networking.Tests
+{
+ public static class IPNetAddressTests
+ {
+ /// <summary>
+ /// Checks IP address formats.
+ /// </summary>
+ /// <param name="address">IP Address.</param>
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
+ [InlineData("192.168.1.2/255.255.255.0")]
+ [InlineData("192.168.1.2/24")]
+ public static void TryParse_ValidIPStrings_True(string address)
+ => Assert.True(IPNetAddress.TryParse(address, out _));
+
+ [Property]
+ public static Property TryParse_IPv4Address_True(IPv4Address address)
+ => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ [Property]
+ public static Property TryParse_IPv6Address_True(IPv6Address address)
+ => IPNetAddress.TryParse(address.Item.ToString(), out _).ToProperty();
+
+ /// <summary>
+ /// All should be invalid address strings.
+ /// </summary>
+ /// <param name="address">Invalid address strings.</param>
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
+ public static void TryParse_InvalidAddressString_False(string address)
+ => Assert.False(IPNetAddress.TryParse(address, out _));
+ }
+}
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index d5268facc..97bf673ae 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -15,10 +15,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 671b8598d..97c14d463 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -57,66 +57,6 @@ namespace Jellyfin.Networking.Tests
}
/// <summary>
- /// Checks IP address formats.
- /// </summary>
- /// <param name="address">IP Address.</param>
- [Theory]
- [InlineData("127.0.0.1")]
- [InlineData("127.0.0.1:123")]
- [InlineData("localhost")]
- [InlineData("localhost:1345")]
- [InlineData("www.google.co.uk")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
- [InlineData("fe80::7add:12ff:febb:c67b%16")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
- [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
- [InlineData("192.168.1.2/255.255.255.0")]
- [InlineData("192.168.1.2/24")]
- public void ValidHostStrings(string address)
- {
- Assert.True(IPHost.TryParse(address, out _));
- }
-
- /// <summary>
- /// Checks IP address formats.
- /// </summary>
- /// <param name="address">IP Address.</param>
- [Theory]
- [InlineData("127.0.0.1")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]")]
- [InlineData("fe80::7add:12ff:febb:c67b%16")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
- [InlineData("fe80::7add:12ff:febb:c67b%16:123")]
- [InlineData("[fe80::7add:12ff:febb:c67b%16]")]
- [InlineData("192.168.1.2/255.255.255.0")]
- [InlineData("192.168.1.2/24")]
- public void ValidIPStrings(string address)
- {
- Assert.True(IPNetAddress.TryParse(address, out _));
- }
-
- /// <summary>
- /// All should be invalid address strings.
- /// </summary>
- /// <param name="address">Invalid address strings.</param>
- [Theory]
- [InlineData("256.128.0.0.0.1")]
- [InlineData("127.0.0.1#")]
- [InlineData("localhost!")]
- [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
- [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
- public void InvalidAddressString(string address)
- {
- Assert.False(IPNetAddress.TryParse(address, out _));
- Assert.False(IPHost.TryParse(address, out _));
- }
-
- /// <summary>
/// Test collection parsing.
/// </summary>
/// <param name="settings">Collection to parse.</param>
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index b37515e78..14bd53db5 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index 71f8c5181..f312933fb 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -166,6 +166,38 @@ namespace Jellyfin.Server.Implementations.Tests.Data
};
}
+ public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
+ {
+ yield return new object[]
+ {
+ string.Empty,
+ Array.Empty<ItemImageInfo>()
+ };
+
+ yield return new object[]
+ {
+ "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
+ new ItemImageInfo[]
+ {
+ new ()
+ {
+ Path = "/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg",
+ Type = ImageType.Primary,
+ DateModified = new DateTime(637452096478512963, DateTimeKind.Utc),
+ Width = 1920,
+ Height = 1080,
+ BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
+ }
+ }
+ };
+
+ yield return new object[]
+ {
+ "|",
+ Array.Empty<ItemImageInfo>()
+ };
+ }
+
[Theory]
[MemberData(nameof(DeserializeImages_Valid_TestData))]
public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected)
@@ -184,6 +216,23 @@ namespace Jellyfin.Server.Implementations.Tests.Data
}
[Theory]
+ [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))]
+ public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected)
+ {
+ var result = _sqliteItemRepository.DeserializeImages(value);
+ Assert.Equal(expected.Length, result.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i].Path, result[i].Path);
+ Assert.Equal(expected[i].Type, result[i].Type);
+ Assert.Equal(expected[i].DateModified, result[i].DateModified);
+ Assert.Equal(expected[i].Width, result[i].Width);
+ Assert.Equal(expected[i].Height, result[i].Height);
+ Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
+ }
+ }
+
+ [Theory]
[MemberData(nameof(DeserializeImages_Valid_TestData))]
public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value)
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 27713d58a..adbca8344 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -24,7 +24,7 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
@@ -42,6 +42,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
+ <ProjectReference Include="..\Jellyfin.Server.Integration.Tests\Jellyfin.Server.Integration.Tests.csproj" />
</ItemGroup>
</Project>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
index 4fa64d8a2..70acbfc40 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -46,12 +47,36 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
[Fact]
public async Task GetPackages_Valid_Success()
{
- IList<PackageInfo> packages = await _installationManager.GetPackages(
+ PackageInfo[] packages = await _installationManager.GetPackages(
"Jellyfin Stable",
"https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
false);
- Assert.Equal(25, packages.Count);
+ Assert.Equal(25, packages.Length);
+ }
+
+ [Fact]
+ public async Task FilterPackages_NameOnly_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ packages = _installationManager.FilterPackages(packages, "Anime").ToArray();
+ Assert.Single(packages);
+ }
+
+ [Fact]
+ public async Task FilterPackages_GuidOnly_Success()
+ {
+ PackageInfo[] packages = await _installationManager.GetPackages(
+ "Jellyfin Stable",
+ "https://repo.jellyfin.org/releases/plugin/manifest-stable.json",
+ false);
+
+ packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
+ Assert.Single(packages);
}
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs
new file mode 100644
index 000000000..c8ce58047
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/EncoderController.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Controller for testing the encoded url.
+ /// </summary>
+ public class EncoderController : BaseJellyfinApiController
+ {
+ /// <summary>
+ /// Tests the url decoding.
+ /// </summary>
+ /// <param name="params">Parameters to echo back in the response.</param>
+ /// <returns>An <see cref="OkResult"/>.</returns>
+ /// <response code="200">Information retrieved.</response>
+ [HttpGet("UrlDecode")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ContentResult TestUrlDecoding([FromQuery] Dictionary<string, string>? @params = null)
+ {
+ return new ContentResult()
+ {
+ Content = (@params != null && @params.Count > 0)
+ ? string.Join("&", @params.Select(x => x.Key + "=" + x.Value))
+ : string.Empty,
+ ContentType = "text/plain; charset=utf-8",
+ StatusCode = 200
+ };
+ }
+
+ /// <summary>
+ /// Tests the url decoding.
+ /// </summary>
+ /// <param name="params">Parameters to echo back in the response.</param>
+ /// <returns>An <see cref="OkResult"/>.</returns>
+ /// <response code="200">Information retrieved.</response>
+ [HttpGet("UrlArrayDecode")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ContentResult TestUrlArrayDecoding([FromQuery] Dictionary<string, string[]>? @params = null)
+ {
+ return new ContentResult()
+ {
+ Content = (@params != null && @params.Count > 0)
+ ? string.Join("&", @params.Select(x => x.Key + "=" + string.Join(',', x.Value)))
+ : string.Empty,
+ ContentType = "text/plain; charset=utf-8",
+ StatusCode = 200
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
new file mode 100644
index 000000000..732b4f050
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -0,0 +1,48 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests
+{
+ /// <summary>
+ /// Defines the test for encoded querystrings in the url.
+ /// </summary>
+ public class EncodedQueryStringTest : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+
+ public EncodedQueryStringTest(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Theory]
+ [InlineData("a=1&b=2&c=3", "a=1&b=2&c=3")] // won't be processed as there is more than 1.
+ [InlineData("a=1", "a=1")] // won't be processed as it has a value
+ [InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
+ [InlineData("a=b&a=c", "a=b")]
+ [InlineData("a%3Db%26a%3Dc", "a=b")]
+ public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Assert.Equal(unencodedUrl, reply);
+ }
+
+ [Theory]
+ [InlineData("a=b&a=c", "a=b,c")]
+ [InlineData("a%3Db%26a%3Dc", "a=b,c")]
+ public async Task Ensure_Array_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ string reply = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ Assert.Equal(unencodedUrl, reply);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 4bf6faef7..af4c22759 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -14,7 +14,7 @@
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 260b99df9..bdcf5cfc8 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -15,7 +15,7 @@
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3" />
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
new file mode 100644
index 000000000..419afb2dc
--- /dev/null
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using Jellyfin.Server.Middleware;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Jellyfin.Server.Tests
+{
+ public static class UrlDecodeQueryFeatureTests
+ {
+ [Theory]
+ [InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
+ [InlineData("random+test", "random test")] // encoded
+ [InlineData("random%20test", "random test")] // encoded
+ [InlineData("++", " ")] // encoded
+ public static void EmptyValueTest(string query, string key)
+ {
+ var dict = new Dictionary<string, StringValues>
+ {
+ { query, StringValues.Empty }
+ };
+ var test = new UrlDecodeQueryFeature(new QueryFeature(new QueryCollection(dict)));
+ Assert.Single(test.Query);
+ var (k, v) = test.Query.First();
+ Assert.Equal(key, k);
+ Assert.Empty(v);
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index 4132205c3..0a04a5c54 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 9ad093a2b..3e726f23d 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -14,8 +14,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
-#pragma warning disable CA5369
-
namespace Jellyfin.XbmcMetadata.Tests.Parsers
{
public class EpisodeNfoProviderTests
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
index 2129f3422..eea8cb50a 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -1,6 +1,4 @@
-#pragma warning disable CA5369
-
-using System;
+using System;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.Audio;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
index 0e61fa2a1..31110dbd7 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
@@ -1,6 +1,4 @@
-#pragma warning disable CA5369
-
-using System;
+using System;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;