aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/codeql-analysis.yml6
-rw-r--r--.github/workflows/commands.yml10
-rw-r--r--.github/workflows/openapi.yml6
-rw-r--r--.npmrc3
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props26
-rw-r--r--Emby.Naming/AudioBook/AudioBookFilePathParser.cs4
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParser.cs2
-rw-r--r--Emby.Naming/Common/NamingOptions.cs42
-rw-r--r--Emby.Naming/TV/EpisodePathParser.cs14
-rw-r--r--Emby.Naming/Video/CleanDateTimeParser.cs2
-rw-r--r--Emby.Naming/Video/ExtraRuleResolver.cs2
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs29
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs52
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs9
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs21
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs23
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json3
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs53
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs6
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs13
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs5
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs7
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs5
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs5
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs12
-rw-r--r--Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs3
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs113
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs53
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs13
-rw-r--r--Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs56
-rw-r--r--Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs56
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs (renamed from Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs)46
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs25
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs27
-rw-r--r--Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs2
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs45
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs46
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs6
-rw-r--r--Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs26
-rw-r--r--Jellyfin.Api/Constants/Policies.cs20
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/ClientLogController.cs3
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs9
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs11
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs5
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs97
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs25
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs116
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs137
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs8
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs7
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs14
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs2
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs11
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs23
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs13
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs32
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs9
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs12
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs3
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs18
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs9
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs37
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs44
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs14
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs9
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs34
-rw-r--r--Jellyfin.Api/Middleware/LanFilteringMiddleware.cs11
-rw-r--r--Jellyfin.Api/Models/UserDtos/CreateUserByName.cs7
-rw-r--r--Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs2
-rw-r--r--Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs2
-rw-r--r--Jellyfin.Data/DayOfWeekHelper.cs11
-rw-r--r--Jellyfin.Data/Entities/User.cs4
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs7
-rw-r--r--Jellyfin.Data/Enums/PreferenceKind.cs7
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs5
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs6
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs147
-rw-r--r--Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs67
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs3
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs89
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs1
-rw-r--r--MediaBrowser.Common/Net/IPNetAddress.cs5
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs7
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs6
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs2
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs10
-rw-r--r--MediaBrowser.Controller/Library/LibraryManagerExtensions.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs86
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs12
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs5
-rw-r--r--MediaBrowser.Model/Tasks/ITaskManager.cs4
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs10
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs3
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs8
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs5
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html26
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs90
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs55
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs2
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs56
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--nuget.config6
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj2
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj2
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--tests/Directory.Build.props23
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs (renamed from tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs)19
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs8
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs59
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs53
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs36
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs80
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj17
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj17
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj17
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj17
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj17
-rw-r--r--tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj16
-rw-r--r--tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj18
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj17
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj17
-rw-r--r--tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj17
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs5
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs19
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj17
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs1
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj17
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj18
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs19
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml6
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs28
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs64
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs40
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs26
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs41
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs25
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs129
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs27
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj16
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj17
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj17
193 files changed, 2014 insertions, 1794 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5779ac3cf..41d59e243 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+ uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+ uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+ uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 5d945c001..75227c57b 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -58,7 +58,7 @@ jobs:
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 4577ff525..5ba53af86 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -103,14 +103,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
+ uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -125,7 +125,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index b7a317000..000000000
--- a/.npmrc
+++ /dev/null
@@ -1,3 +0,0 @@
-registry=https://registry.npmjs.org/
-@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
-always-auth=true \ No newline at end of file
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 5884c7376..20ea7a4ab 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -232,3 +232,4 @@
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
+ - [JPUC1143](https://github.com/Jpuc1143/)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 26f069211..4024f0c05 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,28 +23,28 @@
<PackageVersion Include="libse" Version="3.6.10" />
<PackageVersion Include="LrcParser" Version="2022.529.1" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.2" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
@@ -53,9 +53,9 @@
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
<PackageVersion Include="PlaylistsNET" Version="1.3.1" />
- <PackageVersion Include="prometheus-net.AspNetCore" Version="7.0.0" />
+ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
- <PackageVersion Include="prometheus-net" Version="7.0.0" />
+ <PackageVersion Include="prometheus-net" Version="8.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
@@ -77,7 +77,7 @@
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
- <PackageVersion Include="System.Text.Json" Version="7.0.1" />
+ <PackageVersion Include="System.Text.Json" Version="7.0.2" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.0.0" />
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 7b4429ab1..219599d56 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["chapter"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.ChapterNumber = intValue;
}
@@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["part"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.PartNumber = intValue;
}
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
index 97b34199e..f49c3f0e7 100644
--- a/Emby.Naming/AudioBook/AudioBookNameParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["year"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.Year = intValue;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 54f62a157..e9161a6b7 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -338,7 +338,15 @@ namespace Emby.Naming.Common
}
},
- // This isn't a Kodi naming rule, but the expression below causes false positives,
+ // This isn't a Kodi naming rule, but the expression below causes false episode numbers for
+ // Title Season X Episode X naming schemes.
+ // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi"
+ new EpisodeExpression(@".*[\\\/]((?<seriesname>[^\\/]+?)\s)?[Ss](?:eason)?\s*(?<seasonnumber>[0-9]+)\s+[Ee](?:pisode)?\s*(?<epnumber>[0-9]+).*$")
+ {
+ IsNamed = true
+ },
+
+ // Not a Kodi rule as well, but the expression below also causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
@@ -453,16 +461,6 @@ namespace Emby.Naming.Common
},
};
- EpisodeWithoutSeasonExpressions = new[]
- {
- @"[/\._ \-]()([0-9]+)(-[0-9]+)?"
- };
-
- EpisodeMultiPartExpressions = new[]
- {
- @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)"
- };
-
VideoExtraRules = new[]
{
new ExtraRule(
@@ -798,16 +796,6 @@ namespace Emby.Naming.Common
public EpisodeExpression[] EpisodeExpressions { get; set; }
/// <summary>
- /// Gets or sets list of raw episode without season regular expressions strings.
- /// </summary>
- public string[] EpisodeWithoutSeasonExpressions { get; set; }
-
- /// <summary>
- /// Gets or sets list of raw multi-part episodes regular expressions strings.
- /// </summary>
- public string[] EpisodeMultiPartExpressions { get; set; }
-
- /// <summary>
/// Gets or sets list of video file extensions.
/// </summary>
public string[] VideoFileExtensions { get; set; }
@@ -878,24 +866,12 @@ namespace Emby.Naming.Common
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
- /// Gets list of episode without season regular expressions.
- /// </summary>
- public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
-
- /// <summary>
- /// Gets list of multi-part episode regular expressions.
- /// </summary>
- public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
-
- /// <summary>
/// Compiles raw regex strings into regexes.
/// </summary>
public void Compile()
{
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
- EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
- EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
}
private Regex Compile(string exp)
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index d706be280..8cd5a126e 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -113,7 +113,7 @@ namespace Emby.Naming.TV
if (expression.DateTimeFormats.Length > 0)
{
if (DateTime.TryParseExact(
- match.Groups[0].Value,
+ match.Groups[0].ValueSpan,
expression.DateTimeFormats,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
@@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true;
}
}
- else if (DateTime.TryParse(match.Groups[0].Value, out date))
+ else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
{
result.Year = date.Year;
result.Month = date.Month;
@@ -138,12 +138,12 @@ namespace Emby.Naming.TV
}
else if (expression.IsNamed)
{
- if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
- if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
@@ -158,7 +158,7 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{
- if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EndingEpisodeNumber = num;
}
@@ -170,12 +170,12 @@ namespace Emby.Naming.TV
}
else
{
- if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
- if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index 0ee633dcc..9a6c6e978 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -43,7 +43,7 @@ namespace Emby.Naming.Video
&& match.Groups.Count == 5
&& match.Groups[1].Success
&& match.Groups[2].Success
- && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
+ && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
return true;
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 21d0da364..3219472ef 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -56,7 +56,7 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
- var filename = Path.GetFileName(path);
+ var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 804832040..8247c374d 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -106,6 +106,7 @@ namespace Emby.Naming.Video
}
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+ VideoInfo? primary = null;
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
@@ -118,25 +119,24 @@ namespace Emby.Naming.Video
{
return videos;
}
+
+ if (folderName.Equals(Path.GetFileNameWithoutExtension(video.Files[0].Path.AsSpan()), StringComparison.Ordinal))
+ {
+ primary = video;
+ }
}
// 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));
+ primary ??= videos[0];
+ videos.Remove(primary);
var list = new List<VideoInfo>
{
- videos[0]
+ primary
};
- var alternateVersionsLen = videos.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
- {
- var video = videos[i + 1];
- alternateVersions[i] = video.Files[0];
- }
-
- list[0].AlternateVersions = alternateVersions;
+ list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
list[0].Name = folderName.ToString();
return list;
@@ -176,16 +176,15 @@ namespace Emby.Naming.Video
}
// There are no span overloads for regex unfortunately
- var tmpTestFilename = testFilename.ToString();
- if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+ if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim();
+ testFilename = cleanName.AsSpan().Trim();
}
// The CleanStringParser should have removed common keywords etc.
- return string.IsNullOrEmpty(tmpTestFilename)
+ return testFilename.IsEmpty
|| testFilename[0] == '-'
- || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
}
}
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 985a127d5..a4deeddb7 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -34,14 +32,9 @@ namespace Emby.Server.Implementations.AppBase
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
/// <summary>
- /// The _configuration loaded.
- /// </summary>
- private bool _configurationLoaded;
-
- /// <summary>
/// The _configuration.
/// </summary>
- private BaseApplicationConfiguration _configuration;
+ private BaseApplicationConfiguration? _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
/// <summary>
/// Occurs when [configuration updated].
/// </summary>
- public event EventHandler<EventArgs> ConfigurationUpdated;
+ public event EventHandler<EventArgs>? ConfigurationUpdated;
/// <summary>
/// Occurs when [configuration updating].
/// </summary>
- public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+ public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
/// <summary>
/// Occurs when [named configuration updated].
/// </summary>
- public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+ public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
/// <summary>
/// Gets the type of the configuration.
@@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
{
get
{
- if (_configurationLoaded)
+ if (_configuration is not null)
{
return _configuration;
}
lock (_configurationSyncLock)
{
- if (_configurationLoaded)
+ if (_configuration is not null)
{
return _configuration;
}
- _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
-
- _configurationLoaded = true;
-
- return _configuration;
+ return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
}
}
protected set
{
_configuration = value;
-
- _configurationLoaded = value is not null;
}
}
@@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath;
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock)
{
@@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType)
{
- if (!File.Exists(path))
- {
- return Activator.CreateInstance(configurationType);
- }
-
try
{
- return XmlSerializer.DeserializeFromFile(configurationType, path);
- }
- catch (IOException)
- {
- return Activator.CreateInstance(configurationType);
+ if (File.Exists(path))
+ {
+ return XmlSerializer.DeserializeFromFile(configurationType, path);
+ }
}
- catch (Exception ex)
+ catch (Exception ex) when (ex is not IOException)
{
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
-
- return Activator.CreateInstance(configurationType);
}
+
+ return Activator.CreateInstance(configurationType)
+ ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
}
/// <inheritdoc />
@@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_configurationSyncLock)
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 37a9e7715..d104058cc 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -1184,10 +1184,13 @@ namespace Emby.Server.Implementations
}
}
- // used for closing websockets
- foreach (var session in _sessionManager.Sessions)
+ if (_sessionManager != null)
{
- await session.DisposeAsync().ConfigureAwait(false);
+ // used for closing websockets
+ foreach (var session in _sessionManager.Sessions)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index ff5602f24..6b8b1a620 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Globalization;
using System.IO;
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
/// <summary>
/// Configuration updating event.
/// </summary>
- public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating;
+ public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
/// <summary>
/// Gets the type of the configuration.
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index bc703fe90..602d2a853 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -4477,6 +4477,24 @@ namespace Emby.Server.Implementations.Data
}
}
+ if (query.IncludeInheritedTags.Length > 0)
+ {
+ var paramName = "@IncludeInheritedTags";
+ if (statement is null)
+ {
+ int index = 0;
+ string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
+ whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
+ }
+ else
+ {
+ for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
+ {
+ statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
+ }
+ }
+ }
+
if (query.SeriesStatuses.Length > 0)
{
var statuses = new List<string>();
@@ -5440,6 +5458,9 @@ AND Type = @InternalPersonType)");
list.AddRange(inheritedTags.Select(i => (6, i)));
+ // Remove all invalid values.
+ list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
+
return list;
}
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 6fc7f1ac3..84c21931c 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 4376bd356..90f7568a9 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using MediaBrowser.Common.Configuration;
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 968bf5fa3..c9b41f819 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 1522cd3ae..ef4fa1fd2 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -313,13 +313,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
- private static bool IsIgnored(string filename)
- {
- // Ignore samples
- Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
- return m.Success;
- }
+ private static bool IsIgnored(ReadOnlySpan<char> filename)
+ => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index e874990da..066afb956 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
{
- string episodeTitle = program.Episode?.Title;
+ string episodeTitle = program.Episode.Title;
+ var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
var programInfo = new ProgramInfo
{
ChannelId = program.ChannelId,
EndDate = program.EndDate.UtcDateTime,
- EpisodeNumber = program.Episode?.Episode,
+ EpisodeNumber = program.Episode.Episode,
EpisodeTitle = episodeTitle,
- Genres = program.Categories,
+ Genres = programCategories,
StartDate = program.StartDate.UtcDateTime,
Name = program.Title,
Overview = program.Description,
ProductionYear = program.CopyrightDate?.Year,
- SeasonNumber = program.Episode?.Series,
- IsSeries = program.Episode is not null,
+ SeasonNumber = program.Episode.Series,
+ IsSeries = program.Episode.Series is not null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere is not null,
- IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
- IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating,
- SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+ SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
if (string.IsNullOrWhiteSpace(program.ProgramId))
@@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
Id = c.Id,
Name = c.DisplayName,
- ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source,
+ ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
}).ToList();
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 8ad9e8c71..8bd3c5def 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -118,11 +118,11 @@
"TaskCleanActivityLog": "Borrar log de actividades",
"Undefined": "Indefinido",
"Forced": "Forzado",
- "Default": "Por Defecto",
+ "Default": "Predeterminado",
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
"TaskOptimizeDatabase": "Optimización de base de datos",
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
- "HearingImpaired": "Personas con discapacidad auditiva"
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 695c0f404..87ce07da3 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -82,7 +82,7 @@
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
- "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}",
+ "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
"DeviceOnlineWithName": "{0} telah terhubung",
"NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index e03747cbe..081ba0cc7 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -58,8 +58,8 @@
"NotificationOptionServerRestartRequired": "Server herstart nodig",
"NotificationOptionTaskFailed": "Geplande taak mislukt",
"NotificationOptionUserLockedOut": "Gebruiker is vergrendeld",
- "NotificationOptionVideoPlayback": "Video gestart",
- "NotificationOptionVideoPlaybackStopped": "Video gestopt",
+ "NotificationOptionVideoPlayback": "Afspelen van video gestart",
+ "NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt",
"Photos": "Foto's",
"Playlists": "Afspeellijsten",
"Plugin": "Plug-in",
@@ -95,26 +95,26 @@
"TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
"TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
"TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
- "TaskRefreshChannels": "Vernieuw Kanalen",
+ "TaskRefreshChannels": "Vernieuw kanalen",
"TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
"TaskCleanLogs": "Logboekmap opschonen",
"TaskCleanTranscode": "Transcoderingsmap opschonen",
"TaskUpdatePluginsDescription": "Downloadt en installeert updates van plug-ins waarvoor automatisch bijwerken is ingeschakeld.",
"TaskUpdatePlugins": "Plug-ins bijwerken",
- "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.",
+ "TaskRefreshPeopleDescription": "Update metadata voor acteurs en regisseurs in de media bibliotheek.",
"TaskRefreshPeople": "Personen vernieuwen",
"TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.",
"TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
"TaskRefreshLibrary": "Mediabibliotheek scannen",
- "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
- "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
+ "TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.",
+ "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren",
"TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
"TaskCleanCache": "Cache-map opschonen",
- "TasksChannelsCategory": "Internet Kanalen",
+ "TasksChannelsCategory": "Internetkanalen",
"TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
- "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
+ "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 65cf29e80..839bbcb6d 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -16,14 +16,14 @@
"Folders": "Папки",
"Genres": "Жанры",
"HeaderAlbumArtists": "Исполнители альбома",
- "HeaderContinueWatching": "Продолжение просмотра",
+ "HeaderContinueWatching": "Продолжить просмотр",
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды",
"HeaderFavoriteShows": "Избранные сериалы",
"HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир",
- "HeaderNextUp": "Очередное",
+ "HeaderNextUp": "Следующий",
"HeaderRecordingGroups": "Группы записей",
"HomeVideos": "Домашние видео",
"Inherit": "Наследуемое",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} - неудачна",
"ScheduledTaskStartedWithName": "{0} - запущена",
"ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
- "Shows": "Передачи",
+ "Shows": "Телешоу",
"Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index d845accac..4c23f71ef 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
"External": "Zunanji",
- "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa."
+ "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
+ "HearingImpaired": "Oslabljen sluh"
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index f2212f4dc..7c23254a1 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -123,41 +123,64 @@ namespace Emby.Server.Implementations.Plugins
continue;
}
+ var assemblyLoadContext = new PluginLoadContext(plugin.Path);
+ _assemblyLoadContexts.Add(assemblyLoadContext);
+
+ var assemblies = new List<Assembly>(plugin.DllFiles.Count);
+ var loadedAll = true;
+
foreach (var file in plugin.DllFiles)
{
- Assembly assembly;
try
{
- var assemblyLoadContext = new PluginLoadContext(file);
- _assemblyLoadContexts.Add(assemblyLoadContext);
-
- assembly = assemblyLoadContext.LoadFromAssemblyPath(file);
-
- // Load all required types to verify that the plugin will load
- assembly.GetTypes();
+ assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file));
}
catch (FileLoadException ex)
{
- _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
+ _logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
- continue;
+ loadedAll = false;
+ break;
+ }
+#pragma warning disable CA1031 // Do not catch general exception types
+ catch (Exception ex)
+#pragma warning restore CA1031 // Do not catch general exception types
+ {
+ _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file);
+ ChangePluginState(plugin, PluginStatus.Malfunctioned);
+ loadedAll = false;
+ break;
+ }
+ }
+
+ if (!loadedAll)
+ {
+ continue;
+ }
+
+ foreach (var assembly in assemblies)
+ {
+ try
+ {
+ // Load all required types to verify that the plugin will load
+ assembly.GetTypes();
}
catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
{
- _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
+ _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin", assembly.Location);
ChangePluginState(plugin, PluginStatus.NotSupported);
- continue;
+ break;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
- _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
+ _logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", assembly.Location);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
- continue;
+ break;
}
- _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
+ _logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location);
yield return assembly;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 63f0beb10..6dc20e66b 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -43,9 +41,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
- public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
- public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+ public event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
/// <summary>
/// Gets the list of Scheduled Tasks.
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index aebb55907..4e427b1a4 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -58,7 +56,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// The KeepAlive cancellation token.
/// </summary>
- private CancellationTokenSource _keepAliveCancellationToken;
+ private CancellationTokenSource? _keepAliveCancellationToken;
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
@@ -105,7 +103,7 @@ namespace Emby.Server.Implementations.Session
}
}
- private async Task<SessionInfo> GetSession(HttpContext httpContext, string remoteEndpoint)
+ private async Task<SessionInfo?> GetSession(HttpContext httpContext, string? remoteEndpoint)
{
if (!httpContext.User.Identity?.IsAuthenticated ?? false)
{
@@ -138,8 +136,13 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="sender">The WebSocket.</param>
/// <param name="e">The event arguments.</param>
- private void OnWebSocketClosed(object sender, EventArgs e)
+ private void OnWebSocketClosed(object? sender, EventArgs e)
{
+ if (sender is null)
+ {
+ return;
+ }
+
var webSocket = (IWebSocketConnection)sender;
_logger.LogDebug("WebSocket {0} is closed.", webSocket);
RemoveWebSocket(webSocket);
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 646bafbb5..753e58324 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
ArgumentNullException.ThrowIfNull(x);
-
ArgumentNullException.ThrowIfNull(y);
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 0bd9600b9..5b6c64f63 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -23,15 +21,14 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
}
- private static string GetValue(BaseItem item)
+ private static string? GetValue(BaseItem? item)
{
var hasSeries = item as IHasSeries;
-
return hasSeries?.FindSeriesSortName();
}
}
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 628b9b3dd..19abafe19 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
ArgumentNullException.ThrowIfNull(x);
-
ArgumentNullException.ThrowIfNull(y);
return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index c3df7c47e..2759d20de 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -24,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
return GetDate(x).CompareTo(GetDate(y));
}
@@ -34,7 +32,7 @@ namespace Emby.Server.Implementations.Sorting
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
- private static DateTime GetDate(BaseItem x)
+ private static DateTime GetDate(BaseItem? x)
{
if (x is LiveTvProgram hasStartDate)
{
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 457c06271..89d10f3d2 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -24,10 +22,9 @@ namespace Emby.Server.Implementations.Sorting
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
- public int Compare(BaseItem x, BaseItem y)
+ public int Compare(BaseItem? x, BaseItem? y)
{
ArgumentNullException.ThrowIfNull(x);
-
ArgumentNullException.ThrowIfNull(y);
return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 967f90b55..f0e173f0b 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -42,7 +40,7 @@ namespace Emby.Server.Implementations.TV
throw new ArgumentException("User not found");
}
- string presentationUniqueKey = null;
+ string? presentationUniqueKey = null;
if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
{
if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
@@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.TV
throw new ArgumentException("User not found");
}
- string presentationUniqueKey = null;
+ string? presentationUniqueKey = null;
int? limit = null;
if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
{
@@ -168,7 +166,7 @@ namespace Emby.Server.Implementations.TV
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
})
.Select(i => i.GetEpisodeFunction())
- .Where(i => i is not null);
+ .Where(i => i is not null)!;
}
private static string GetUniqueSeriesKey(Episode episode)
@@ -185,7 +183,7 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up.
/// </summary>
/// <returns>Task{Episode}.</returns>
- private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
+ private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
{
var lastQuery = new InternalItemsQuery(user)
{
@@ -209,7 +207,7 @@ namespace Emby.Server.Implementations.TV
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
- Episode GetEpisode()
+ Episode? GetEpisode()
{
var nextQuery = new InternalItemsQuery(user)
{
diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
index d4b1ffb06..741b88ea9 100644
--- a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
+++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -29,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
{
- var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress;
+ var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
// Loopback will be on LAN, so we can accept null.
if (ip is null || _networkManager.IsInLocalNetwork(ip))
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
deleted file mode 100644
index 8e5e66d64..000000000
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-using System.Security.Claims;
-using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth
-{
- /// <summary>
- /// Base authorization handler.
- /// </summary>
- /// <typeparam name="T">Type of Authorization Requirement.</typeparam>
- public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
- where T : IAuthorizationRequirement
- {
- private readonly IUserManager _userManager;
- private readonly INetworkManager _networkManager;
- private readonly IHttpContextAccessor _httpContextAccessor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- protected BaseAuthorizationHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- {
- _userManager = userManager;
- _networkManager = networkManager;
- _httpContextAccessor = httpContextAccessor;
- }
-
- /// <summary>
- /// Validate authenticated claims.
- /// </summary>
- /// <param name="claimsPrincipal">Request claims.</param>
- /// <param name="ignoreSchedule">Whether to ignore parental control.</param>
- /// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
- /// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
- /// <returns>Validated claim status.</returns>
- protected bool ValidateClaims(
- ClaimsPrincipal claimsPrincipal,
- bool ignoreSchedule = false,
- bool localAccessOnly = false,
- bool requiredDownloadPermission = false)
- {
- // ApiKey is currently global admin, always allow.
- var isApiKey = claimsPrincipal.GetIsApiKey();
- if (isApiKey)
- {
- return true;
- }
-
- // Ensure claim has userId.
- var userId = claimsPrincipal.GetUserId();
- if (userId.Equals(default))
- {
- return false;
- }
-
- // Ensure userId links to a valid user.
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return false;
- }
-
- // Ensure user is not disabled.
- if (user.HasPermission(PermissionKind.IsDisabled))
- {
- return false;
- }
-
- var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
- && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
-
- // User cannot access remotely and user is remote
- if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
- {
- return false;
- }
-
- if (localAccessOnly && !isInLocalNetwork)
- {
- return false;
- }
-
- // User attempting to access out of parental control hours.
- if (!ignoreSchedule
- && !user.HasPermission(PermissionKind.IsAdministrator)
- && !user.IsParentalScheduleAllowed())
- {
- return false;
- }
-
- // User attempting to download without permission.
- if (requiredDownloadPermission
- && !user.HasPermission(PermissionKind.EnableContentDownloading))
- {
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
index be77b7a4e..b1d97e4a1 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -1,4 +1,8 @@
using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -9,8 +13,12 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
/// <summary>
/// Default authorization handler.
/// </summary>
- public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
+ public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
+ private readonly IUserManager _userManager;
+ private readonly INetworkManager _networkManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
/// </summary>
@@ -21,21 +29,56 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
{
+ _userManager = userManager;
+ _networkManager = networkManager;
+ _httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{
- var validated = ValidateClaims(context.User);
- if (validated)
+ var isApiKey = context.User.GetIsApiKey();
+ var userId = context.User.GetUserId();
+ // This likely only happens during the wizard, so skip the default checks and let any other handlers do it
+ if (!isApiKey && userId.Equals(default))
+ {
+ return Task.CompletedTask;
+ }
+
+ var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
+ && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ // User cannot access remotely and user is remote
+ if (!isInLocalNetwork && !user.HasPermission(PermissionKind.EnableRemoteAccess))
+ {
+ context.Fail();
+ return Task.CompletedTask;
+ }
+
+ // Admins can do everything
+ if (isApiKey || context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
+ return Task.CompletedTask;
}
- else
+
+ // It's not great to have this check, but parental schedule must usually be honored except in a few rare cases
+ if (requirement.ValidateParentalSchedule && !user.IsParentalScheduleAllowed())
{
context.Fail();
+ return Task.CompletedTask;
+ }
+
+ // Only succeed if the requirement isn't a subclass as any subclassed requirement will handle success in its own handler
+ if (requirement.GetType() == typeof(DefaultAuthorizationRequirement))
+ {
+ context.Succeed(requirement);
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
index 7cea00b69..5ba1bc330 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
@@ -7,5 +7,18 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
/// </summary>
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultAuthorizationRequirement"/> class.
+ /// </summary>
+ /// <param name="validateParentalSchedule">A value indicating whether to validate parental schedule.</param>
+ public DefaultAuthorizationRequirement(bool validateParentalSchedule = true)
+ {
+ ValidateParentalSchedule = validateParentalSchedule;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether to ignore parental schedule.
+ /// </summary>
+ public bool ValidateParentalSchedule { get; }
}
}
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
deleted file mode 100644
index b61680ab1..000000000
--- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.DownloadPolicy
-{
- /// <summary>
- /// Download authorization handler.
- /// </summary>
- public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="DownloadHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public DownloadHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
- {
- var validated = ValidateClaims(context.User);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
deleted file mode 100644
index b0a72a9de..000000000
--- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.DownloadPolicy
-{
- /// <summary>
- /// The download permission requirement.
- /// </summary>
- public class DownloadRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
deleted file mode 100644
index 31482a930..000000000
--- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
-{
- /// <summary>
- /// Ignore parental control schedule and allow before startup wizard has been completed.
- /// </summary>
- public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
- {
- private readonly IConfigurationManager _configurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- public FirstTimeOrIgnoreParentalControlSetupHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor,
- IConfigurationManager configurationManager)
- : base(userManager, networkManager, httpContextAccessor)
- {
- _configurationManager = configurationManager;
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement)
- {
- if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
- {
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
-
- var validated = ValidateClaims(context.User, ignoreSchedule: true);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
deleted file mode 100644
index 00aaec334..000000000
--- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
-{
- /// <summary>
- /// First time setup or ignore parental controls requirement.
- /// </summary>
- public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
deleted file mode 100644
index dd0bd4ec2..000000000
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
-{
- /// <summary>
- /// Authorization handler for requiring first time setup or default privileges.
- /// </summary>
- public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
- {
- private readonly IConfigurationManager _configurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
- /// </summary>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public FirstTimeSetupOrDefaultHandler(
- IConfigurationManager configurationManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- _configurationManager = configurationManager;
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
- {
- if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
- {
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
-
- var validated = ValidateClaims(context.User);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
deleted file mode 100644
index f7366bd7a..000000000
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
-{
- /// <summary>
- /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
- /// </summary>
- public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs
deleted file mode 100644
index 51ba637b6..000000000
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
-{
- /// <summary>
- /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
- /// </summary>
- public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 90b76ee99..28ba25850 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,39 +1,36 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
+namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
{
/// <summary>
- /// Authorization handler for requiring first time setup or elevated privileges.
+ /// Authorization handler for requiring first time setup or default privileges.
/// </summary>
- public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+ public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement>
{
private readonly IConfigurationManager _configurationManager;
+ private readonly IUserManager _userManager;
/// <summary>
- /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
+ /// Initializes a new instance of the <see cref="FirstTimeSetupHandler" /> class.
/// </summary>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public FirstTimeSetupOrElevatedHandler(
+ public FirstTimeSetupHandler(
IConfigurationManager configurationManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
+ IUserManager userManager)
{
_configurationManager = configurationManager;
+ _userManager = userManager;
}
/// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
@@ -41,14 +38,27 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
return Task.CompletedTask;
}
- var validated = ValidateClaims(context.User);
- if (validated && context.User.IsInRole(UserRoles.Administrator))
+ if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
+ {
+ context.Fail();
+ return Task.CompletedTask;
+ }
+
+ if (!requirement.ValidateParentalSchedule)
{
context.Succeed(requirement);
+ return Task.CompletedTask;
}
- else
+
+ var user = _userManager.GetUserById(context.User.GetUserId());
+ if (user is null)
{
- context.Fail();
+ throw new ResourceNotFoundException();
+ }
+
+ if (user.IsParentalScheduleAllowed())
+ {
+ context.Succeed(requirement);
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs
new file mode 100644
index 000000000..6252a2feb
--- /dev/null
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupRequirement.cs
@@ -0,0 +1,25 @@
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+
+namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
+{
+ /// <summary>
+ /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
+ /// </summary>
+ public class FirstTimeSetupRequirement : DefaultAuthorizationRequirement
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FirstTimeSetupRequirement"/> class.
+ /// </summary>
+ /// <param name="validateParentalSchedule">A value indicating whether to ignore parental schedule.</param>
+ /// <param name="requireAdmin">A value indicating whether administrator role is required.</param>
+ public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) : base(validateParentalSchedule)
+ {
+ RequireAdmin = requireAdmin;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether administrator role is required.
+ /// </summary>
+ public bool RequireAdmin { get; }
+ }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
deleted file mode 100644
index a7623556a..000000000
--- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
-{
- /// <summary>
- /// Escape schedule controls handler.
- /// </summary>
- public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public IgnoreParentalControlHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
- {
- var validated = ValidateClaims(context.User, ignoreSchedule: true);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
deleted file mode 100644
index cdad74270..000000000
--- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
-{
- /// <summary>
- /// Escape schedule controls requirement.
- /// </summary>
- public class IgnoreParentalControlRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
index 14722aa57..6ed6fc90b 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
@@ -1,7 +1,7 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -10,27 +10,38 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
/// <summary>
/// Local access or require elevated privileges handler.
/// </summary>
- public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
+ public class LocalAccessOrRequiresElevationHandler : AuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
{
+ private readonly INetworkManager _networkManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
/// <summary>
/// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
/// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public LocalAccessOrRequiresElevationHandler(
- IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
{
+ _networkManager = networkManager;
+ _httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
{
- var validated = ValidateClaims(context.User, localAccessOnly: true);
- if (validated || context.User.IsInRole(UserRoles.Administrator))
+ var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
+
+ // Loopback will be on LAN, so we can accept null.
+ if (ip is null || _networkManager.IsInLocalNetwork(ip))
+ {
+ context.Succeed(requirement);
+
+ return Task.CompletedTask;
+ }
+
+ if (context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
index d9c64d01c..f633c69d8 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
{
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
deleted file mode 100644
index d772ec554..000000000
--- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.LocalAccessPolicy
-{
- /// <summary>
- /// Local access handler.
- /// </summary>
- public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public LocalAccessHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
- {
- var validated = ValidateClaims(context.User, localAccessOnly: true);
- if (validated)
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
deleted file mode 100644
index 761127fa4..000000000
--- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.LocalAccessPolicy
-{
- /// <summary>
- /// The local access authorization requirement.
- /// </summary>
- public class LocalAccessRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
deleted file mode 100644
index b235c4b63..000000000
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-
-namespace Jellyfin.Api.Auth.RequiresElevationPolicy
-{
- /// <summary>
- /// Authorization handler for requiring elevated privileges.
- /// </summary>
- public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
- public RequiresElevationHandler(
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
- {
- }
-
- /// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
- {
- var validated = ValidateClaims(context.User);
- if (validated && context.User.IsInRole(UserRoles.Administrator))
- {
- context.Succeed(requirement);
- }
- else
- {
- context.Fail();
- }
-
- return Task.CompletedTask;
- }
- }
-}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs
deleted file mode 100644
index cfff1cc0c..000000000
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationRequirement.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-
-namespace Jellyfin.Api.Auth.RequiresElevationPolicy
-{
- /// <summary>
- /// The authorization requirement for requiring elevated privileges in the authorization handler.
- /// </summary>
- public class RequiresElevationRequirement : IAuthorizationRequirement
- {
- }
-}
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
index cdd7d8a52..75ec9fcec 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
@@ -1,19 +1,17 @@
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
/// <summary>
/// Default authorization handler.
/// </summary>
- public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
+ public class SyncPlayAccessHandler : AuthorizationHandler<SyncPlayAccessRequirement>
{
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager;
@@ -23,14 +21,9 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
+ IUserManager userManager)
{
_syncPlayManager = syncPlayManager;
_userManager = userManager;
@@ -39,27 +32,20 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
{
- if (!ValidateClaims(context.User))
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
var userId = context.User.GetUserId();
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{
- if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
- || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
+ if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups
|| _syncPlayManager.IsUserActive(userId))
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{
@@ -67,10 +53,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{
@@ -79,10 +61,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
@@ -90,14 +68,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
- }
- else
- {
- context.Fail();
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
index 6fab4c0ad..220b223b3 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
@@ -1,12 +1,12 @@
-using Jellyfin.Data.Enums;
-using Microsoft.AspNetCore.Authorization;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
/// <summary>
/// The default authorization requirement.
/// </summary>
- public class SyncPlayAccessRequirement : IAuthorizationRequirement
+ public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement
{
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
new file mode 100644
index 000000000..e72bec46f
--- /dev/null
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
@@ -0,0 +1,42 @@
+using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Library;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.UserPermissionPolicy
+{
+ /// <summary>
+ /// User permission authorization handler.
+ /// </summary>
+ public class UserPermissionHandler : AuthorizationHandler<UserPermissionRequirement>
+ {
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserPermissionHandler"/> class.
+ /// </summary>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public UserPermissionHandler(IUserManager userManager)
+ {
+ _userManager = userManager;
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
+ {
+ var user = _userManager.GetUserById(context.User.GetUserId());
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ if (user.HasPermission(requirement.RequiredPermission))
+ {
+ context.Succeed(requirement);
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs
new file mode 100644
index 000000000..4694556eb
--- /dev/null
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs
@@ -0,0 +1,26 @@
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Api.Auth.UserPermissionPolicy
+{
+ /// <summary>
+ /// The user permission requirement.
+ /// </summary>
+ public class UserPermissionRequirement : DefaultAuthorizationRequirement
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserPermissionRequirement"/> class.
+ /// </summary>
+ /// <param name="requiredPermission">The required <see cref="PermissionKind"/>.</param>
+ /// <param name="validateParentalSchedule">Whether to validate the user's parental schedule.</param>
+ public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule)
+ {
+ RequiredPermission = requiredPermission;
+ }
+
+ /// <summary>
+ /// Gets the required user permission.
+ /// </summary>
+ public PermissionKind RequiredPermission { get; }
+ }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 5a5a2bf46..53841b0c4 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -6,11 +6,6 @@ namespace Jellyfin.Api.Constants;
public static class Policies
{
/// <summary>
- /// Policy name for default authorization.
- /// </summary>
- public const string DefaultAuthorization = "DefaultAuthorization";
-
- /// <summary>
/// Policy name for requiring first time setup or elevated privileges.
/// </summary>
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
@@ -74,4 +69,19 @@ public static class Policies
/// Policy name for accessing a SyncPlay group.
/// </summary>
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
+
+ /// <summary>
+ /// Policy name for accessing collection management.
+ /// </summary>
+ public const string CollectionManagement = "CollectionManagement";
+
+ /// <summary>
+ /// Policy name for accessing LiveTV.
+ /// </summary>
+ public const string LiveTvAccess = "LiveTvAccess";
+
+ /// <summary>
+ /// Policy name for managing LiveTV.
+ /// </summary>
+ public const string LiveTvManagement = "LiveTvManagement";
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 069e7311b..c9d2f67f9 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// The artists controller.
/// </summary>
[Route("Artists")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class ArtistsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -119,6 +118,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -126,7 +126,7 @@ public class ArtistsController : BaseJellyfinApiController
User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- if (userId.HasValue && !userId.Equals(default))
+ if (!userId.Value.Equals(default))
{
user = _userManager.GetUserById(userId.Value);
}
@@ -322,6 +322,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -329,7 +330,7 @@ public class ArtistsController : BaseJellyfinApiController
User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- if (userId.HasValue && !userId.Equals(default))
+ if (!userId.Value.Equals(default))
{
user = _userManager.GetUserById(userId.Value);
}
@@ -463,11 +464,12 @@ public class ArtistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions().AddClientFields(User);
var item = _libraryManager.GetArtist(name, dtoOptions);
- if (userId.HasValue && !userId.Value.Equals(default))
+ if (!userId.Value.Equals(default))
{
var user = _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 573b7069c..b5c4d8346 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Channels Controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class ChannelsController : BaseJellyfinApiController
{
private readonly IChannelManager _channelManager;
@@ -61,11 +60,12 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] bool? supportsMediaDeletion,
[FromQuery] bool? isFavorite)
{
+ userId = RequestHelpers.GetUserId(User, userId);
return _channelManager.GetChannels(new ChannelQuery
{
Limit = limit,
StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
+ UserId = userId.Value,
SupportsLatestItems = supportsLatestItems,
SupportsMediaDeletion = supportsMediaDeletion,
IsFavorite = isFavorite
@@ -125,7 +125,8 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -199,7 +200,8 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs
index 21c31bc93..2c5dbacbb 100644
--- a/Jellyfin.Api/Controllers/ClientLogController.cs
+++ b/Jellyfin.Api/Controllers/ClientLogController.cs
@@ -1,7 +1,6 @@
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.ClientLogDtos;
using MediaBrowser.Controller.ClientEvent;
@@ -15,7 +14,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Client log controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class ClientLogController : BaseJellyfinApiController
{
private const int MaxDocumentSize = 1_000_000;
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 5a4a9bf07..2db04afb8 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers;
/// The collection controller.
/// </summary>
[Route("Collections")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize(Policy = Policies.CollectionManagement)]
public class CollectionController : BaseJellyfinApiController
{
private readonly ICollectionManager _collectionManager;
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index d53d7cefd..9007dfc41 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Api.Controllers;
/// Configuration Controller.
/// </summary>
[Route("System")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class ConfigurationController : BaseJellyfinApiController
{
private readonly IServerConfigurationManager _configurationManager;
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index f7e978bad..076084c7a 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
@@ -48,7 +47,7 @@ public class DashboardController : BaseJellyfinApiController
[HttpGet("web/ConfigurationPages")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 497862236..aa0dff212 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
using Jellyfin.Data.Dtos;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
@@ -48,6 +49,7 @@ public class DevicesController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 49d87a362..6f0006832 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
@@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Display Preferences Controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class DisplayPreferencesController : BaseJellyfinApiController
{
private readonly IDisplayPreferencesManager _displayPreferencesManager;
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index b68849171..4d8b4de24 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -9,7 +9,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
@@ -36,7 +35,7 @@ namespace Jellyfin.Api.Controllers;
/// Dynamic hls controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class DynamicHlsController : BaseJellyfinApiController
{
private const string DefaultVodEncoderPreset = "veryfast";
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 2378aada5..dac07429f 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -18,7 +19,7 @@ namespace Jellyfin.Api.Controllers;
/// Filters controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class FilterController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -52,7 +53,8 @@ public class FilterController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -144,7 +146,8 @@ public class FilterController : BaseJellyfinApiController
[FromQuery] bool? isSeries,
[FromQuery] bool? recursive)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 28ebe2047..eb03b514c 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// The genres controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class GenresController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -91,11 +90,12 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
+ User? user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -132,7 +132,7 @@ public class GenresController : BaseJellyfinApiController
QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
- || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
+ || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
{
result = _libraryManager.GetMusicGenres(query);
}
@@ -156,6 +156,7 @@ public class GenresController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -171,7 +172,7 @@ public class GenresController : BaseJellyfinApiController
item ??= new Genre();
- if (userId is null || userId.Value.Equals(default))
+ if (userId.Value.Equals(default))
{
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 085115e1c..d7cec865e 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -80,7 +79,7 @@ public class HlsSegmentController : BaseJellyfinApiController
/// <response code="200">Hls video playlist returned.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
@@ -106,7 +105,7 @@ public class HlsSegmentController : BaseJellyfinApiController
/// <response code="204">Encoding stopped successfully.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("Videos/ActiveEncodings")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult StopEncodingProcess(
[FromQuery, Required] string deviceId,
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index cc824c65a..3c5f18af5 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -88,9 +88,10 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -99,12 +100,22 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
- var user = _userManager.GetUserById(userId);
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
+
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false))
{
@@ -116,7 +127,7 @@ public class ImageController : BaseJellyfinApiController
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -137,9 +148,10 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -148,12 +160,22 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
- var user = _userManager.GetUserById(userId);
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
+
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false))
{
@@ -165,7 +187,7 @@ public class ImageController : BaseJellyfinApiController
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@@ -186,7 +208,7 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -230,7 +252,7 @@ public class ImageController : BaseJellyfinApiController
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -332,6 +354,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage(
@@ -344,6 +367,11 @@ public class ImageController : BaseJellyfinApiController
return NotFound();
}
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
+
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false))
{
@@ -369,6 +397,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImageByIndex(
@@ -382,6 +411,11 @@ public class ImageController : BaseJellyfinApiController
return NotFound();
}
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
+
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false))
{
@@ -432,7 +466,7 @@ public class ImageController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
[HttpGet("Items/{itemId}/Images")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
@@ -1753,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController
[AcceptsImageFile]
public async Task<ActionResult> UploadCustomSplashscreen()
{
+ if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
+ {
+ return BadRequest("Incorrect ContentType.");
+ }
+
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false))
{
- var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
-
- if (!mimeType.HasValue)
- {
- return BadRequest("Error reading mimetype from uploaded image");
- }
-
- var extension = MimeTypes.ToExtension(mimeType.Value);
- if (string.IsNullOrEmpty(extension))
- {
- return BadRequest("Error converting mimetype to an image extension");
- }
-
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
brandingOptions.SplashscreenLocation = filePath;
@@ -1930,10 +1956,10 @@ public class ImageController : BaseJellyfinApiController
}
var responseHeaders = new Dictionary<string, string>
- {
- { "transferMode.dlna.org", "Interactive" },
- { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
- };
+ {
+ { "transferMode.dlna.org", "Interactive" },
+ { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
+ };
if (!imageInfo.IsLocalFile && item is not null)
{
@@ -2096,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController
return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
+
+ internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension)
+ {
+ extension = null;
+ if (string.IsNullOrEmpty(contentType))
+ {
+ return false;
+ }
+
+ if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
+ && parsedValue.MediaType.HasValue
+ && MimeTypes.IsImage(parsedValue.MediaType.Value))
+ {
+ extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
+ return extension is not null;
+ }
+
+ return false;
+ }
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 89592bade..4dc2a4253 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
@@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// The instant mix controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class InstantMixController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -75,7 +75,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -111,7 +112,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var album = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -147,7 +149,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -182,7 +185,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -218,7 +222,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -254,7 +259,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -327,7 +333,8 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index c2ce4e67e..b030e74dd 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers;
/// Item lookup controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class ItemLookupController : BaseJellyfinApiController
{
private readonly IProviderManager _providerManager;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 134974dbe..728e62810 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,11 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers;
/// The items controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class ItemsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -240,8 +240,9 @@ public class ItemsController : BaseJellyfinApiController
{
var isApiKey = User.GetIsApiKey();
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
- var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default)
- ? _userManager.GetUserById(userId.Value)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = !isApiKey && !userId.Value.Equals(default)
+ ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
: null;
// beyond this point, we're either using an api key or we have a valid user
@@ -815,6 +816,11 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool excludeActiveSessions = false)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
var parentIdGuid = parentId ?? Guid.Empty;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 830f84849..bf59febed 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities;
@@ -95,7 +96,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
[HttpGet("Items/{itemId}/File")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile("video/*", "audio/*")]
@@ -116,7 +117,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="200">Critic reviews returned.</response>
/// <returns>The list of critic reviews.</returns>
[HttpGet("Items/{itemId}/CriticReviews")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[Obsolete("This endpoint is obsolete.")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
@@ -134,7 +135,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>The item theme songs.</returns>
[HttpGet("Items/{itemId}/ThemeSongs")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeSongs(
@@ -142,12 +143,13 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
+ ? (userId.Value.Equals(default)
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
@@ -200,7 +202,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>The item theme videos.</returns>
[HttpGet("Items/{itemId}/ThemeVideos")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeVideos(
@@ -208,12 +210,13 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
+ ? (userId.Value.Equals(default)
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
@@ -266,7 +269,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>The item theme videos.</returns>
[HttpGet("Items/{itemId}/ThemeMedia")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<AllThemeMediaResult> GetThemeMedia(
[FromRoute, Required] Guid itemId,
@@ -283,6 +286,11 @@ public class LibraryController : BaseJellyfinApiController
userId,
inheritFromParent);
+ if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+ {
+ return NotFound();
+ }
+
return new AllThemeMediaResult
{
ThemeSongsResult = themeSongs?.Value,
@@ -321,7 +329,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="401">Unauthorized access.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items/{itemId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItem(Guid itemId)
@@ -350,7 +358,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="401">Unauthorized access.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
@@ -392,13 +400,14 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="200">Item counts returned.</response>
/// <returns>Item counts.</returns>
[HttpGet("Items/Counts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ItemCounts> GetItemCounts(
[FromQuery] Guid? userId,
[FromQuery] bool? isFavorite)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -426,12 +435,13 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>Item parents.</returns>
[HttpGet("Items/{itemId}/Ancestors")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
var item = _libraryManager.GetItemById(itemId);
+ userId = RequestHelpers.GetUserId(User, userId);
if (item is null)
{
@@ -440,7 +450,7 @@ public class LibraryController : BaseJellyfinApiController
var baseItemDtos = new List<BaseItemDto>();
- var user = userId is null || userId.Value.Equals(default)
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -452,6 +462,10 @@ public class LibraryController : BaseJellyfinApiController
if (user is not null)
{
parent = TranslateParentItem(parent, user);
+ if (parent is null)
+ {
+ break;
+ }
}
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
@@ -509,7 +523,7 @@ public class LibraryController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
[HttpPost("Library/Series/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
{
@@ -539,7 +553,7 @@ public class LibraryController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
[HttpPost("Library/Movies/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
{
@@ -580,7 +594,7 @@ public class LibraryController : BaseJellyfinApiController
/// <response code="204">Report success.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Media/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
{
@@ -657,7 +671,7 @@ public class LibraryController : BaseJellyfinApiController
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
@@ -666,18 +680,24 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
+ ? (userId.Value.Equals(default)
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
if (item is Episode || (item is IItemByName && item is not MusicArtist))
{
return new QueryResult<BaseItemDto>();
}
- var user = userId is null || userId.Value.Equals(default)
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -802,32 +822,32 @@ public class LibraryController : BaseJellyfinApiController
Type = type,
MetadataFetchers = plugins
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray(),
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary)
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray(),
ImageFetchers = plugins
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray(),
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary)
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray(),
SupportedImageTypes = plugins
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
- .Distinct()
- .ToArray(),
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
+ .Distinct()
+ .ToArray(),
DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
});
@@ -920,13 +940,13 @@ public class LibraryController : BaseJellyfinApiController
if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
{
return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
- || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
- || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
+ || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
}
return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
+ || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
}
var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
@@ -934,7 +954,7 @@ public class LibraryController : BaseJellyfinApiController
.ToArray();
return metadataOptions.Length == 0
- || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
}
private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 21b424346..96fc91f93 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -17,14 +17,12 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -95,7 +93,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<LiveTvInfo> GetLiveTvInfo()
{
return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
@@ -131,7 +129,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpGet("Channels")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
[FromQuery] ChannelType? type,
[FromQuery] Guid? userId,
@@ -155,6 +153,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -163,7 +162,7 @@ public class LiveTvController : BaseJellyfinApiController
new LiveTvChannelQuery
{
ChannelType = type,
- UserId = userId ?? Guid.Empty,
+ UserId = userId.Value,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -182,7 +181,7 @@ public class LiveTvController : BaseJellyfinApiController
dtoOptions,
CancellationToken.None);
- var user = userId is null || userId.Value.Equals(default)
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -210,10 +209,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var item = channelId.Equals(default)
@@ -251,7 +251,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
[HttpGet("Recordings")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
[FromQuery] string? channelId,
[FromQuery] Guid? userId,
@@ -273,6 +273,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isLibraryItem,
[FromQuery] bool enableTotalRecordCount = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -281,7 +282,7 @@ public class LiveTvController : BaseJellyfinApiController
new RecordingQuery
{
ChannelId = channelId,
- UserId = userId ?? Guid.Empty,
+ UserId = userId.Value,
StartIndex = startIndex,
Limit = limit,
Status = status,
@@ -322,7 +323,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
[HttpGet("Recordings/Series")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[Obsolete("This endpoint is obsolete.")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
@@ -365,7 +366,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
[HttpGet("Recordings/Groups")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[Obsolete("This endpoint is obsolete.")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
@@ -381,10 +382,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
[HttpGet("Recordings/Folders")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var folders = _liveTvManager.GetRecordingFolders(user);
@@ -403,10 +405,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
@@ -425,10 +428,9 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -443,7 +445,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpGet("Timers/{timerId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
{
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
@@ -459,7 +461,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpGet("Timers/Defaults")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
{
return string.IsNullOrEmpty(programId)
@@ -479,7 +481,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpGet("Timers")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
[FromQuery] string? channelId,
[FromQuery] string? seriesTimerId,
@@ -533,7 +535,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpGet("Programs")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId,
@@ -563,7 +565,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool enableTotalRecordCount = true)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -616,7 +619,7 @@ public class LiveTvController : BaseJellyfinApiController
/// </returns>
[HttpPost("Programs")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
{
var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
@@ -682,7 +685,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Recommended epgs returned.</response>
/// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
[HttpGet("Programs/Recommended")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
[FromQuery] Guid? userId,
@@ -702,7 +705,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -734,13 +738,14 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Program returned.</response>
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
[HttpGet("Programs/{programId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetProgram(
[FromRoute, Required] string programId,
[FromQuery] Guid? userId)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -755,13 +760,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpDelete("Recordings/{recordingId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
+ public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
-
var item = _libraryManager.GetItemById(recordingId);
if (item is null)
{
@@ -783,11 +786,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Timers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
return NoContent();
}
@@ -800,12 +802,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Timers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -817,11 +818,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer created.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Timers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -834,7 +834,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="404">Series timer not found.</response>
/// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
[HttpGet("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
@@ -856,7 +856,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Timers returned.</response>
/// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
[HttpGet("SeriesTimers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
{
@@ -876,11 +876,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Timer cancelled.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
return NoContent();
}
@@ -893,12 +892,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Series timer updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -910,11 +908,10 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Series timer info created.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("SeriesTimers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -925,7 +922,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="groupId">Group id.</param>
/// <returns>A <see cref="NotFoundResult"/>.</returns>
[HttpGet("Recordings/Groups/{groupId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("This endpoint is obsolete.")]
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
@@ -939,7 +936,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Guid info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
[HttpGet("GuideInfo")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<GuideInfo> GetGuideInfo()
{
@@ -953,7 +950,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created tuner host returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
[HttpPost("TunerHosts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
{
@@ -967,7 +964,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Tuner host deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("TunerHosts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteTunerHost([FromQuery] string? id)
{
@@ -983,7 +980,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Default listings provider info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
[HttpGet("ListingProviders/Default")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
{
@@ -1000,7 +997,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created listings provider returned.</response>
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
[HttpPost("ListingProviders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
@@ -1026,7 +1023,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="204">Listing provider deleted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("ListingProviders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteListingProvider([FromQuery] string? id)
{
@@ -1044,7 +1041,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Available lineups returned.</response>
/// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
[HttpGet("ListingProviders/Lineups")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
[FromQuery] string? id,
@@ -1061,7 +1058,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Available countries returned.</response>
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries()
@@ -1082,7 +1079,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Channel mapping options returned.</response>
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
[HttpGet("ChannelMappingOptions")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
{
@@ -1120,7 +1117,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
{
@@ -1133,7 +1130,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <response code="200">Tuner host types returned.</response>
/// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
[HttpGet("TunerHosts/Types")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
{
@@ -1148,7 +1145,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
[HttpGet("Tuners/Discover")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
{
@@ -1208,26 +1205,4 @@ public class LiveTvController : BaseJellyfinApiController
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
-
- private async Task AssertUserCanManageLiveTv()
- {
- var user = _userManager.GetUserById(User.GetUserId());
- var session = await _sessionManager.LogSessionActivity(
- User.GetClient(),
- User.GetVersion(),
- User.GetDeviceId(),
- User.GetDevice(),
- HttpContext.GetNormalizedRemoteIp().ToString(),
- user).ConfigureAwait(false);
-
- if (session.UserId.Equals(default))
- {
- throw new SecurityException("Anonymous live tv management is not allowed.");
- }
-
- if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
- {
- throw new SecurityException("The current user does not have permission to manage live tv.");
- }
- }
}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index eee7df3af..da24616ff 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
@@ -25,7 +24,7 @@ namespace Jellyfin.Api.Controllers;
/// The media info controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class MediaInfoController : BaseJellyfinApiController
{
private readonly IMediaSourceManager _mediaSourceManager;
@@ -133,6 +132,7 @@ public class MediaInfoController : BaseJellyfinApiController
// Copy params from posted body
// TODO clean up when breaking API compatibility.
userId ??= playbackInfoDto?.UserId;
+ userId = RequestHelpers.GetUserId(User, userId);
maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
@@ -254,10 +254,12 @@ public class MediaInfoController : BaseJellyfinApiController
[FromQuery] bool? enableDirectPlay,
[FromQuery] bool? enableDirectStream)
{
+ userId ??= openLiveStreamDto?.UserId;
+ userId = RequestHelpers.GetUserId(User, userId);
var request = new LiveStreamRequest
{
OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
- UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
+ UserId = userId.Value,
PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 4c30dd2b3..e1145481f 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Movies controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class MoviesController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -68,7 +68,8 @@ public class MoviesController : BaseJellyfinApiController
[FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 302f138eb..435457af6 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// The music genres controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class MusicGenresController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -91,11 +90,12 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
+ User? user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -145,6 +145,7 @@ public class MusicGenresController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions().AddClientFields(User);
MusicGenre? item;
@@ -158,7 +159,12 @@ public class MusicGenresController : BaseJellyfinApiController
item = _libraryManager.GetMusicGenre(genreName);
}
- if (userId.HasValue && !userId.Value.Equals(default))
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ if (!userId.Value.Equals(default))
{
var user = _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 3cb3caadb..0ba5e995f 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers;
/// Package Controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 9fb6da527..b4c6f490a 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,8 +1,8 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Persons controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class PersonsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -78,11 +78,12 @@ public class PersonsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
+ User? user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -118,6 +119,7 @@ public class PersonsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -127,7 +129,7 @@ public class PersonsController : BaseJellyfinApiController
return NotFound();
}
- if (userId.HasValue && !userId.Value.Equals(default))
+ if (!userId.Value.Equals(default))
{
var user = _userManager.GetUserById(userId.Value);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 11e589301..c6dbea5e2 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -4,8 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
using MediaBrowser.Controller.Dto;
@@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Playlists controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class PlaylistsController : BaseJellyfinApiController
{
private readonly IPlaylistManager _playlistManager;
@@ -82,11 +82,13 @@ public class PlaylistsController : BaseJellyfinApiController
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
}
+ userId ??= createPlaylistRequest?.UserId ?? default;
+ userId = RequestHelpers.GetUserId(User, userId);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
Name = name ?? createPlaylistRequest?.Name,
ItemIdList = ids,
- UserId = userId ?? createPlaylistRequest?.UserId ?? default,
+ UserId = userId.Value,
MediaType = mediaType ?? createPlaylistRequest?.MediaType
}).ConfigureAwait(false);
@@ -108,7 +110,8 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId)
{
- await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
+ userId = RequestHelpers.GetUserId(User, userId);
+ await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 18d6ebf1e..8ad553bcb 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -2,7 +2,6 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// Playstate controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class PlaystateController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -77,6 +76,11 @@ public class PlaystateController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var item = _libraryManager.GetItemById(itemId);
@@ -89,6 +93,11 @@ public class PlaystateController : BaseJellyfinApiController
foreach (var additionalUserInfo in session.AdditionalUsers)
{
var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ if (additionalUser is null)
+ {
+ return NotFound();
+ }
+
UpdatePlayedStatus(additionalUser, item, true, datePlayed);
}
@@ -109,6 +118,11 @@ public class PlaystateController : BaseJellyfinApiController
public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var item = _libraryManager.GetItemById(itemId);
@@ -121,6 +135,11 @@ public class PlaystateController : BaseJellyfinApiController
foreach (var additionalUserInfo in session.AdditionalUsers)
{
var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ if (additionalUser is null)
+ {
+ return NotFound();
+ }
+
UpdatePlayedStatus(additionalUser, item, false, null);
}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 5a037d7a6..4726cf066 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -21,7 +21,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Plugins controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class PluginsController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index a58e85b2b..d7e54b5b6 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
@@ -111,22 +112,16 @@ public class QuickConnectController : BaseJellyfinApiController
/// <response code="403">Unknown user id.</response>
/// <returns>Boolean indicating if the authorization was successful.</returns>
[HttpPost("Authorize")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null)
{
- var currentUserId = User.GetUserId();
- var actualUserId = userId ?? currentUserId;
-
- if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
- {
- return Forbid("Unknown user id");
- }
+ userId = RequestHelpers.GetUserId(User, userId);
try
{
- return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
+ return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
}
catch (AuthenticationException)
{
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 445c5594f..5c77db240 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -56,7 +56,7 @@ public class RemoteImageController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>Remote Image Result.</returns>
[HttpGet("Items/{itemId}/RemoteImages")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
@@ -121,7 +121,7 @@ public class RemoteImageController : BaseJellyfinApiController
/// <response code="404">Item not found.</response>
/// <returns>List of remote image providers.</returns>
[HttpGet("Items/{itemId}/RemoteImages/Providers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 46b4920ca..f638c31c3 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -26,7 +27,7 @@ namespace Jellyfin.Api.Controllers;
/// Search controller.
/// </summary>
[Route("Search/Hints")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class SearchController : BaseJellyfinApiController
{
private readonly ISearchEngine _searchEngine;
@@ -99,6 +100,7 @@ public class SearchController : BaseJellyfinApiController
[FromQuery] bool includeStudios = true,
[FromQuery] bool includeArtists = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var result = _searchEngine.GetSearchHints(new SearchQuery
{
Limit = limit,
@@ -109,7 +111,7 @@ public class SearchController : BaseJellyfinApiController
IncludePeople = includePeople,
IncludeStudios = includeStudios,
StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
+ UserId = userId.Value,
IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = excludeItemTypes,
MediaTypes = mediaTypes,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index ef3364478..e93456de6 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -56,7 +56,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="200">List of sessions returned.</response>
/// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
[HttpGet("Sessions")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<SessionInfo>> GetSessions(
[FromQuery] Guid? controllableByUserId,
@@ -75,6 +75,10 @@ public class SessionController : BaseJellyfinApiController
result = result.Where(i => i.SupportsRemoteControl);
var user = _userManager.GetUserById(controllableByUserId.Value);
+ if (user is null)
+ {
+ return NotFound();
+ }
if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
@@ -119,7 +123,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Viewing")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DisplayContent(
[FromRoute, Required] string sessionId,
@@ -158,7 +162,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Instruction sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> Play(
[FromRoute, Required] string sessionId,
@@ -201,7 +205,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Playstate command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> SendPlaystateCommand(
[FromRoute, Required] string sessionId,
@@ -232,7 +236,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">System command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/System/{command}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> SendSystemCommand(
[FromRoute, Required] string sessionId,
@@ -258,7 +262,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">General command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Command/{command}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> SendGeneralCommand(
[FromRoute, Required] string sessionId,
@@ -286,7 +290,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Full general command sent to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Command")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> SendFullGeneralCommand(
[FromRoute, Required] string sessionId,
@@ -316,7 +320,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Message sent.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/Message")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> SendMessageCommand(
[FromRoute, Required] string sessionId,
@@ -345,7 +349,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">User added to session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/{sessionId}/User/{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession(
[FromRoute, Required] string sessionId,
@@ -363,7 +367,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">User removed from session.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Sessions/{sessionId}/User/{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession(
[FromRoute, Required] string sessionId,
@@ -385,7 +389,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Capabilities posted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Capabilities")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> PostCapabilities(
[FromQuery] string? id,
@@ -419,7 +423,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Capabilities updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Capabilities/Full")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> PostFullCapabilities(
[FromQuery] string? id,
@@ -443,7 +447,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Session reported to server.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Viewing")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ReportViewing(
[FromQuery] string? sessionId,
@@ -461,7 +465,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="204">Session end reported to server.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Logout")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ReportSessionEnded()
{
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 799be2ae8..f434f60f5 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -21,7 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Studios controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class StudiosController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -87,11 +86,12 @@ public class StudiosController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
+ User? user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -140,10 +140,11 @@ public class StudiosController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions().AddClientFields(User);
var item = _libraryManager.GetStudio(name);
- if (userId.HasValue && !userId.Equals(default))
+ if (!userId.Equals(default))
{
var user = _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index fd0a71f9e..e38421338 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -114,7 +114,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -134,7 +134,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -164,7 +164,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
[HttpGet("Providers/Subtitles/Subtitles/{id}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesFile("text/*")]
@@ -322,7 +322,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Subtitle playlist retrieved.</response>
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -463,7 +463,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Information retrieved.</response>
/// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
[HttpGet("FallbackFont/Fonts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<FontFile> GetFallbackFontList()
{
@@ -514,7 +514,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Fallback font file retrieved.</response>
/// <returns>The fallback font file.</returns>
[HttpGet("FallbackFont/Fonts/{name}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("font/*")]
public ActionResult GetFallbackFont([FromRoute, Required] string name)
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index c5c429757..5b808f257 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -19,7 +18,7 @@ namespace Jellyfin.Api.Controllers;
/// The suggestions controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class SuggestionsController : BaseJellyfinApiController
{
private readonly IDtoService _dtoService;
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index b0b2e2d6d..4ab705f40 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -172,7 +172,7 @@ public class SystemController : BaseJellyfinApiController
/// <response code="200">Information retrieved.</response>
/// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
[HttpGet("Endpoint")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<EndPointInfo> GetEndpointInfo()
{
@@ -210,7 +210,7 @@ public class SystemController : BaseJellyfinApiController
/// <response code="200">Information retrieved.</response>
/// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
[HttpGet("WakeOnLanInfo")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[Obsolete("This endpoint is obsolete.")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 115efcd8f..b5b640620 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,5 +1,4 @@
using System;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
@@ -14,7 +13,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// The trailers controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class TrailersController : BaseJellyfinApiController
{
private readonly ItemsController _itemsController;
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 2be32095e..7d23281f2 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -2,8 +2,8 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers;
/// The tv shows controller.
/// </summary>
[Route("Shows")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class TvShowsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -88,6 +88,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool disableFirstEpisode = false,
[FromQuery] bool enableRewatching = false)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var options = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -99,7 +100,7 @@ public class TvShowsController : BaseJellyfinApiController
ParentId = parentId,
SeriesId = seriesId,
StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
+ UserId = userId.Value,
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
@@ -107,7 +108,7 @@ public class TvShowsController : BaseJellyfinApiController
},
options);
- var user = userId is null || userId.Value.Equals(default)
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -145,7 +146,8 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -216,7 +218,8 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
@@ -332,7 +335,8 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 6946caa2b..12d033ae6 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -82,7 +81,7 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesAudioFile]
@@ -107,11 +106,7 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
-
- if (!userId.HasValue || userId.Value.Equals(default))
- {
- userId = User.GetUserId();
- }
+ userId = RequestHelpers.GetUserId(User, userId);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 7f184f31e..b0973b8a1 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -81,7 +81,7 @@ public class UserController : BaseJellyfinApiController
/// <response code="200">Users returned.</response>
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
[HttpGet]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<UserDto>> GetUsers(
[FromQuery] bool? isHidden,
@@ -147,6 +147,11 @@ public class UserController : BaseJellyfinApiController
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
return NoContent();
@@ -251,7 +256,7 @@ public class UserController : BaseJellyfinApiController
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
[HttpPost("{userId}/Password")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -281,8 +286,8 @@ public class UserController : BaseJellyfinApiController
{
var success = await _userManager.AuthenticateUser(
user.Username,
- request.CurrentPw,
- request.CurrentPw,
+ request.CurrentPw ?? string.Empty,
+ request.CurrentPw ?? string.Empty,
HttpContext.GetNormalizedRemoteIp().ToString(),
false).ConfigureAwait(false);
@@ -292,7 +297,7 @@ public class UserController : BaseJellyfinApiController
}
}
- await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
+ await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false);
var currentToken = User.GetToken();
@@ -312,7 +317,7 @@ public class UserController : BaseJellyfinApiController
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
[HttpPost("{userId}/EasyPassword")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -338,7 +343,7 @@ public class UserController : BaseJellyfinApiController
}
else
{
- await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
+ await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false);
}
return NoContent();
@@ -354,7 +359,7 @@ public class UserController : BaseJellyfinApiController
/// <response code="403">User update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
[HttpPost("{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
@@ -362,13 +367,17 @@ public class UserController : BaseJellyfinApiController
[FromRoute, Required] Guid userId,
[FromBody, Required] UserDto updateUser)
{
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
- var user = _userManager.GetUserById(userId);
-
if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
@@ -398,6 +407,10 @@ public class UserController : BaseJellyfinApiController
[FromBody, Required] UserPolicy newPolicy)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
// If removing admin access
if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
@@ -440,7 +453,7 @@ public class UserController : BaseJellyfinApiController
/// <response code="403">User configuration update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{userId}/Configuration")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration(
@@ -526,7 +539,7 @@ public class UserController : BaseJellyfinApiController
/// <response code="400">Token is not owned by a user.</response>
/// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
[HttpGet("Me")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<UserDto> GetCurrentUser()
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 556cf3894..1233995b4 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -28,7 +27,7 @@ namespace Jellyfin.Api.Controllers;
/// User library controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class UserLibraryController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -79,10 +78,18 @@ public class UserLibraryController : BaseJellyfinApiController
public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
@@ -102,6 +109,11 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
var item = _libraryManager.GetUserRootFolder();
var dtoOptions = new DtoOptions().AddClientFields(User);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
@@ -119,10 +131,18 @@ public class UserLibraryController : BaseJellyfinApiController
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -200,10 +220,18 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -230,10 +258,18 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
var item = itemId.Equals(default)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -275,6 +311,10 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] bool groupItems = true)
{
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
if (!isPlayed.HasValue)
{
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index aa7ba8891..838b43234 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
@@ -23,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// User views controller.
/// </summary>
[Route("")]
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class UserViewsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 01a319879..c0ec646ed 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -100,16 +100,17 @@ public class VideosController : BaseJellyfinApiController
/// <response code="200">Additional parts returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
[HttpGet("{itemId}/AdditionalParts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
- var user = userId is null || userId.Value.Equals(default)
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
+ ? (userId.Value.Equals(default)
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
@@ -155,7 +156,12 @@ public class VideosController : BaseJellyfinApiController
if (video.LinkedAlternateVersions.Length == 0)
{
- video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId);
+ video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId);
+ }
+
+ if (video is null)
+ {
+ return NotFound();
}
foreach (var link in video.GetLinkedAlternateVersions())
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 2e5fdc146..74370db50 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -24,7 +23,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Years controller.
/// </summary>
-[Authorize(Policy = Policies.DefaultAuthorization)]
+[Authorize]
public class YearsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
@@ -86,11 +85,12 @@ public class YearsController : BaseJellyfinApiController
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
+ User? user = userId.Value.Equals(default)
? null
: _userManager.GetUserById(userId.Value);
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
@@ -172,6 +172,7 @@ public class YearsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var item = _libraryManager.GetYear(year);
if (item is null)
{
@@ -181,7 +182,7 @@ public class YearsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions()
.AddClientFields(User);
- if (userId.HasValue && !userId.Value.Equals(default))
+ if (!userId.Value.Equals(default))
{
var user = _userManager.GetUserById(userId.Value);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index df37d96c6..5910d8073 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -200,7 +200,7 @@ public class MediaInfoHelper
options.SubtitleStreamIndex = subtitleStreamIndex;
}
- var user = _userManager.GetUserById(userId);
+ var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
if (!enableDirectPlay)
{
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 3ce2b834d..57098edba 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -11,6 +11,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
@@ -56,6 +57,32 @@ public static class RequestHelpers
}
/// <summary>
+ /// Checks if the user can access a user.
+ /// </summary>
+ /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param>
+ /// <param name="userId">The user id.</param>
+ /// <returns>A <see cref="bool"/> whether the user can access the user.</returns>
+ internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId)
+ {
+ var authenticatedUserId = claimsPrincipal.GetUserId();
+
+ // UserId not provided, fall back to authenticated user id.
+ if (userId is null || userId.Value.Equals(default))
+ {
+ return authenticatedUserId;
+ }
+
+ // User must be administrator to access another user.
+ var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
+ if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator)
+ {
+ throw new SecurityException("Forbidden");
+ }
+
+ return userId.Value;
+ }
+
+ /// <summary>
/// Checks if the user can update an entry.
/// </summary>
/// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
@@ -81,6 +108,11 @@ public static class RequestHelpers
}
var user = userManager.GetUserById(userId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
return user.EnableUserPreferenceAccess;
}
@@ -98,7 +130,7 @@ public static class RequestHelpers
if (session is null)
{
- throw new ArgumentException("Session not found.");
+ throw new ResourceNotFoundException("Session not found.");
}
return session;
diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
index 7b05351e3..9c2194faf 100644
--- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
+++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
@@ -1,6 +1,6 @@
-using System.Net;
using System.Threading.Tasks;
using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
@@ -32,9 +32,14 @@ public class LanFilteringMiddleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
{
- var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
+ if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
+ {
+ await _next(httpContext).ConfigureAwait(false);
+ return;
+ }
- if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
+ var host = httpContext.GetNormalizedRemoteIp();
+ if (!networkManager.IsInLocalNetwork(host))
{
return;
}
diff --git a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
index 0503c5d57..6b6d9682b 100644
--- a/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
+++ b/Jellyfin.Api/Models/UserDtos/CreateUserByName.cs
@@ -1,4 +1,6 @@
-namespace Jellyfin.Api.Models.UserDtos;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Api.Models.UserDtos;
/// <summary>
/// The create user by name request body.
@@ -8,7 +10,8 @@ public class CreateUserByName
/// <summary>
/// Gets or sets the username.
/// </summary>
- public string? Name { get; set; }
+ [Required]
+ required public string Name { get; set; }
/// <summary>
/// Gets or sets the password.
diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
index ebe9297ea..a0631fd07 100644
--- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
+++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordDto.cs
@@ -11,5 +11,5 @@ public class ForgotPasswordDto
/// Gets or sets the entered username to have its password reset.
/// </summary>
[Required]
- public string? EnteredUsername { get; set; }
+ required public string EnteredUsername { get; set; }
}
diff --git a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
index 2949efe29..79b8a5d63 100644
--- a/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
+++ b/Jellyfin.Api/Models/UserDtos/ForgotPasswordPinDto.cs
@@ -11,5 +11,5 @@ public class ForgotPasswordPinDto
/// Gets or sets the entered pin to have the password reset.
/// </summary>
[Required]
- public string? Pin { get; set; }
+ required public string Pin { get; set; }
}
diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs
index b7ba30180..82abfb831 100644
--- a/Jellyfin.Data/DayOfWeekHelper.cs
+++ b/Jellyfin.Data/DayOfWeekHelper.cs
@@ -17,5 +17,16 @@ namespace Jellyfin.Data
_ => new[] { (DayOfWeek)day }
};
}
+
+ public static bool Contains(this DynamicDayOfWeek dynamicDayOfWeek, DayOfWeek dayOfWeek)
+ {
+ return dynamicDayOfWeek switch
+ {
+ DynamicDayOfWeek.Everyday => true,
+ DynamicDayOfWeek.Weekday => dayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Friday,
+ DynamicDayOfWeek.Weekend => dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday,
+ _ => (DayOfWeek)dynamicDayOfWeek == dayOfWeek
+ };
+ }
}
}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index eb59e70f3..606e1b542 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -508,6 +508,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
+ Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
}
/// <summary>
@@ -525,8 +526,9 @@ namespace Jellyfin.Data.Entities
{
var localTime = date.ToLocalTime();
var hour = localTime.TimeOfDay.TotalHours;
+ var currentDayOfWeek = localTime.DayOfWeek;
- return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
+ return schedule.DayOfWeek.Contains(currentDayOfWeek)
&& hour >= schedule.StartHour
&& hour <= schedule.EndHour;
}
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 7d5200874..40280b95e 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -108,6 +108,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// Whether the server should force transcoding on remote connections for the user.
/// </summary>
- ForceRemoteSourceTranscoding = 20
+ ForceRemoteSourceTranscoding = 20,
+
+ /// <summary>
+ /// Whether the user can create, modify and delete collections.
+ /// </summary>
+ EnableCollectionManagement = 21
}
}
diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Data/Enums/PreferenceKind.cs
index a54d789af..d2b412e45 100644
--- a/Jellyfin.Data/Enums/PreferenceKind.cs
+++ b/Jellyfin.Data/Enums/PreferenceKind.cs
@@ -63,6 +63,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// A list of ordered views.
/// </summary>
- OrderedViews = 11
+ OrderedViews = 11,
+
+ /// <summary>
+ /// A list of allowed tags.
+ /// </summary>
+ AllowedTags = 12
}
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 8b15d6823..a4b4c1959 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -9,6 +9,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using Jellyfin.Extensions;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Devices;
@@ -185,6 +186,10 @@ namespace Jellyfin.Server.Implementations.Devices
if (userId.HasValue)
{
var user = _userManager.GetUserById(userId.Value);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index dc9d78857..c4756433e 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -369,8 +369,10 @@ namespace Jellyfin.Server.Implementations.Users
EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
+ EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
+ AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
@@ -684,6 +686,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+ user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
@@ -696,6 +699,7 @@ namespace Jellyfin.Server.Implementations.Users
// TODO: fix this at some point
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+ user.SetPreference(PreferenceKind.AllowedTags, policy.AllowedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
@@ -736,7 +740,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name));
}
- private static bool IsValidUsername(string name)
+ private static bool IsValidUsername(ReadOnlySpan<char> name)
{
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index e9af1cf83..9867c9e47 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -5,19 +5,15 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
+using System.Security.Claims;
using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.AnonymousLanAccessPolicy;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
-using Jellyfin.Api.Auth.DownloadPolicy;
-using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
-using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
-using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
-using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
-using Jellyfin.Api.Auth.LocalAccessPolicy;
-using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Auth.SyncPlayAccessPolicy;
+using Jellyfin.Api.Auth.UserPermissionPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
using Jellyfin.Api.Formatters;
@@ -56,117 +52,38 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
+ // The default handler must be first so that it is evaluated first
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, DownloadHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrDefaultHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeOrIgnoreParentalControlSetupHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
- serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
+
return serviceCollection.AddAuthorizationCore(options =>
{
- options.AddPolicy(
- Policies.DefaultAuthorization,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new DefaultAuthorizationRequirement());
- });
- options.AddPolicy(
- Policies.Download,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new DownloadRequirement());
- });
- options.AddPolicy(
- Policies.FirstTimeSetupOrDefault,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement());
- });
- options.AddPolicy(
- Policies.FirstTimeSetupOrElevated,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
- });
- options.AddPolicy(
- Policies.IgnoreParentalControl,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new IgnoreParentalControlRequirement());
- });
- options.AddPolicy(
- Policies.FirstTimeSetupOrIgnoreParentalControl,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement());
- });
- options.AddPolicy(
- Policies.LocalAccessOnly,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new LocalAccessRequirement());
- });
- options.AddPolicy(
- Policies.LocalAccessOrRequiresElevation,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement());
- });
+ options.DefaultPolicy = new AuthorizationPolicyBuilder()
+ .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
+ .AddRequirements(new DefaultAuthorizationRequirement())
+ .Build();
+
+ options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
+ options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement));
+ options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading));
+ options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false));
+ options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement());
+ options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, false));
+ options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false));
+ options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess));
+ options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement));
+ options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement());
+ options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
+ options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
+ options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
+ options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
options.AddPolicy(
Policies.RequiresElevation,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new RequiresElevationRequirement());
- });
- options.AddPolicy(
- Policies.SyncPlayHasAccess,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
- });
- options.AddPolicy(
- Policies.SyncPlayCreateGroup,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
- });
- options.AddPolicy(
- Policies.SyncPlayJoinGroup,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
- });
- options.AddPolicy(
- Policies.SyncPlayIsInGroup,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
- });
- options.AddPolicy(
- Policies.AnonymousLanAccessPolicy,
- policy =>
- {
- policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new AnonymousLanAccessRequirement());
- });
+ policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
+ .RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
});
}
@@ -334,6 +251,14 @@ namespace Jellyfin.Server.Extensions
});
}
+ private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)
+ {
+ authorizationOptions.AddPolicy(policyName, policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizationRequirement);
+ });
+ }
+
/// <summary>
/// Sets up the proxy configuration based on the addresses in <paramref name="allowedProxies"/>.
/// </summary>
diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
index 4af670e9a..fb9f6d0a6 100644
--- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
+++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
@@ -18,11 +18,17 @@ namespace Jellyfin.Server.Filters
{
var requiredScopes = new List<string>();
+ var requiresAuth = false;
// Add all method scopes.
foreach (var attribute in context.MethodInfo.GetCustomAttributes(true))
{
- if (attribute is AuthorizeAttribute authorizeAttribute
- && authorizeAttribute.Policy is not null
+ if (attribute is not AuthorizeAttribute authorizeAttribute)
+ {
+ continue;
+ }
+
+ requiresAuth = true;
+ if (authorizeAttribute.Policy is not null
&& !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
{
requiredScopes.Add(authorizeAttribute.Policy);
@@ -35,8 +41,13 @@ namespace Jellyfin.Server.Filters
{
foreach (var attribute in controllerAttributes)
{
- if (attribute is AuthorizeAttribute authorizeAttribute
- && authorizeAttribute.Policy is not null
+ if (attribute is not AuthorizeAttribute authorizeAttribute)
+ {
+ continue;
+ }
+
+ requiresAuth = true;
+ if (authorizeAttribute.Policy is not null
&& !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
{
requiredScopes.Add(authorizeAttribute.Policy);
@@ -44,35 +55,37 @@ namespace Jellyfin.Server.Filters
}
}
- if (requiredScopes.Count != 0)
+ if (!requiresAuth)
{
- if (!operation.Responses.ContainsKey("401"))
- {
- operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
- }
+ return;
+ }
- if (!operation.Responses.ContainsKey("403"))
- {
- operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
- }
+ if (!operation.Responses.ContainsKey("401"))
+ {
+ operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
+ }
- var scheme = new OpenApiSecurityScheme
+ if (!operation.Responses.ContainsKey("403"))
+ {
+ operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
+ }
+
+ var scheme = new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
{
- Reference = new OpenApiReference
- {
- Type = ReferenceType.SecurityScheme,
- Id = AuthenticationSchemes.CustomAuthentication
- }
- };
+ Type = ReferenceType.SecurityScheme,
+ Id = AuthenticationSchemes.CustomAuthentication
+ }
+ };
- operation.Security = new List<OpenApiSecurityRequirement>
+ operation.Security = new List<OpenApiSecurityRequirement>
+ {
+ new OpenApiSecurityRequirement
{
- new OpenApiSecurityRequirement
- {
- [scheme] = requiredScopes
- }
- };
- }
+ [scheme] = requiredScopes
+ }
+ };
}
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 23fb9e370..8a6ab7932 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations
/// </summary>
private static readonly Type[] _preStartupMigrationTypes =
{
- typeof(PreStartupRoutines.CreateNetworkConfiguration)
+ typeof(PreStartupRoutines.CreateNetworkConfiguration),
+ typeof(PreStartupRoutines.MigrateMusicBrainzTimeout)
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
new file mode 100644
index 000000000..14b51bd4c
--- /dev/null
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs
@@ -0,0 +1,89 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using Emby.Server.Implementations;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines;
+
+/// <inheritdoc />
+public class MigrateMusicBrainzTimeout : IMigrationRoutine
+{
+ private readonly ServerApplicationPaths _applicationPaths;
+ private readonly ILogger<MigrateMusicBrainzTimeout> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateMusicBrainzTimeout"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public MigrateMusicBrainzTimeout(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger<MigrateMusicBrainzTimeout>();
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0");
+
+ /// <inheritdoc />
+ public string Name => nameof(MigrateMusicBrainzTimeout);
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ string path = Path.Combine(_applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.MusicBrainz.xml");
+ if (!File.Exists(path))
+ {
+ _logger.LogDebug("No MusicBrainz plugin configuration file found, skipping");
+ return;
+ }
+
+ var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
+ using var xmlReader = XmlReader.Create(path);
+ var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
+
+ if (oldPluginConfiguration is not null)
+ {
+ var newPluginConfiguration = new PluginConfiguration();
+ newPluginConfiguration.Server = oldPluginConfiguration.Server;
+ newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName;
+ var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0;
+ newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit;
+
+ var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
+ var xmlWriterSettings = new XmlWriterSettings { Indent = true };
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
+ }
+ }
+
+#pragma warning disable
+ public sealed class OldMusicBrainzConfiguration
+ {
+ private string _server = string.Empty;
+
+ private long _rateLimit = 0L;
+
+ public string Server
+ {
+ get => _server;
+ set => _server = value.TrimEnd('/');
+ }
+
+ public long RateLimit
+ {
+ get => _rateLimit;
+ set => _rateLimit = value;
+ }
+
+ public bool ReplaceArtistName { get; set; }
+ }
+#pragma warning restore
+
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index ea2f03302..9bf1e6b80 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -163,6 +163,7 @@ namespace Jellyfin.Server.Migrations.Routines
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+ user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs
index ac3396a9f..de72d978e 100644
--- a/MediaBrowser.Common/Net/IPNetAddress.cs
+++ b/MediaBrowser.Common/Net/IPNetAddress.cs
@@ -167,6 +167,11 @@ namespace MediaBrowser.Common.Net
address = address.MapToIPv4();
}
+ if (address.AddressFamily != AddressFamily)
+ {
+ return false;
+ }
+
var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength);
return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix;
}
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 89aafc84f..22453f0f7 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CA1002
using System.Collections.Generic;
@@ -28,7 +27,7 @@ namespace MediaBrowser.Controller.Dto
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
/// <returns>BaseItemDto.</returns>
- BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null);
+ BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null);
/// <summary>
/// Gets the base item dtos.
@@ -38,7 +37,7 @@ namespace MediaBrowser.Controller.Dto
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
/// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
- IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
+ IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null);
/// <summary>
/// Gets the item by name dto.
@@ -48,6 +47,6 @@ namespace MediaBrowser.Controller.Dto
/// <param name="taggedItems">The list of tagged items.</param>
/// <param name="user">The user.</param>
/// <returns>The item dto.</returns>
- BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
+ BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null);
}
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index f2c2007f7..3d683052f 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1607,6 +1607,12 @@ namespace MediaBrowser.Controller.Entities
return false;
}
+ var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
+ if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
return true;
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index a1e531904..a51299284 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -26,6 +26,7 @@ namespace MediaBrowser.Controller.Entities
EnableTotalRecordCount = true;
ExcludeArtistIds = Array.Empty<Guid>();
ExcludeInheritedTags = Array.Empty<string>();
+ IncludeInheritedTags = Array.Empty<string>();
ExcludeItemIds = Array.Empty<Guid>();
ExcludeItemTypes = Array.Empty<BaseItemKind>();
ExcludeTags = Array.Empty<string>();
@@ -95,6 +96,8 @@ namespace MediaBrowser.Controller.Entities
public string[] ExcludeInheritedTags { get; set; }
+ public string[] IncludeInheritedTags { get; set; }
+
public IReadOnlyList<string> Genres { get; set; }
public bool? IsSpecialSeason { get; set; }
@@ -368,6 +371,7 @@ namespace MediaBrowser.Controller.Entities
}
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+ IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
User = user;
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 882abc927..66210cb6c 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -104,7 +104,7 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
{
- return true;
+ return user.HasPermission(PermissionKind.IsAdministrator) || user.HasPermission(PermissionKind.EnableCollectionManagement);
}
public override bool IsSaveLocalMetadataEnabled()
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 993e3e18f..37b4afcf3 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -47,14 +45,14 @@ namespace MediaBrowser.Controller.Library
/// <param name="id">The id.</param>
/// <returns>The user with the specified Id, or <c>null</c> if the user doesn't exist.</returns>
/// <exception cref="ArgumentException"><c>id</c> is an empty Guid.</exception>
- User GetUserById(Guid id);
+ User? GetUserById(Guid id);
/// <summary>
/// Gets the name of the user by.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>User.</returns>
- User GetUserByName(string name);
+ User? GetUserByName(string name);
/// <summary>
/// Renames the user.
@@ -128,7 +126,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <returns>UserDto.</returns>
- UserDto GetUserDto(User user, string remoteEndPoint = null);
+ UserDto GetUserDto(User user, string? remoteEndPoint = null);
/// <summary>
/// Authenticates the user.
@@ -139,7 +137,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="remoteEndPoint">Remove endpoint to use.</param>
/// <param name="isUserSession">Specifies if a user session.</param>
/// <returns>User wrapped in awaitable task.</returns>
- Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
+ Task<User?> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
/// <summary>
/// Starts the forgot password process.
diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
index 7bc8fa5ab..6d2c3c3d2 100644
--- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
+++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -9,7 +7,7 @@ namespace MediaBrowser.Controller.Library
{
public static class LibraryManagerExtensions
{
- public static BaseItem GetItemById(this ILibraryManager manager, string id)
+ public static BaseItem? GetItemById(this ILibraryManager manager, string id)
{
return manager.GetItemById(new Guid(id));
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 98a6f47ba..732b9e459 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -559,6 +559,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
{
+ // Use Apple's aac encoder if available as it provides best audio quality
+ if (_mediaEncoder.SupportsEncoder("aac_at"))
+ {
+ return "aac_at";
+ }
+
// Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support
if (_mediaEncoder.SupportsEncoder("libfdk_aac"))
{
@@ -2786,6 +2792,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "deinterlace_qsv=mode=2";
}
+ else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "yadif_videotoolbox={0}:-1:0",
+ doubleRateDeint ? "1" : "0");
+ }
return string.Empty;
}
@@ -4423,6 +4436,75 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the parameter of Apple VideoToolBox filter chain.
+ /// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="vidEncoder">Video encoder to use.</param>
+ /// <returns>The tuple contains three lists: main, sub and overlay filters.</returns>
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFilterChain(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidEncoder)
+ {
+ if (!string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ return (null, null, null);
+ }
+
+ var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder);
+
+ if (!options.EnableHardwareEncoding)
+ {
+ return swFilterChain;
+ }
+
+ if (_mediaEncoder.EncoderVersion.CompareTo(new Version("5.0.0")) < 0)
+ {
+ // All features used here requires ffmpeg 5.0 or later, fallback to software filters if using an old ffmpeg
+ return swFilterChain;
+ }
+
+ var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+ var doDeintH2645 = doDeintH264 || doDeintHevc;
+ var inW = state.VideoStream?.Width;
+ var inH = state.VideoStream?.Height;
+ var reqW = state.BaseRequest.Width;
+ var reqH = state.BaseRequest.Height;
+ var reqMaxW = state.BaseRequest.MaxWidth;
+ var reqMaxH = state.BaseRequest.MaxHeight;
+ var threeDFormat = state.MediaSource.Video3DFormat;
+ var newfilters = new List<string>();
+ var noOverlay = swFilterChain.OverlayFilters.Count == 0;
+ var supportsHwDeint = _mediaEncoder.SupportsFilter("yadif_videotoolbox");
+ // fallback to software filters if we are using filters not supported by hardware yet.
+ var useHardwareFilters = noOverlay && (!doDeintH2645 || supportsHwDeint);
+
+ if (!useHardwareFilters)
+ {
+ return swFilterChain;
+ }
+
+ // ffmpeg cannot use videotoolbox to scale
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ newfilters.Add(swScaleFilter);
+
+ // hwupload on videotoolbox encoders can automatically convert AVFrame into its CVPixelBuffer equivalent
+ // videotoolbox will automatically convert the CVPixelBuffer to a pixel format the encoder supports, so we don't have to set a pixel format explicitly here
+ // This will reduce CPU usage significantly on UHD videos with 10 bit colors because we bypassed the ffmpeg pixel format conversion
+ newfilters.Add("hwupload");
+
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox");
+ newfilters.Add(deintFilter);
+ }
+
+ return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
+ }
+
+ /// <summary>
/// Gets the parameter of video processing filters.
/// </summary>
/// <param name="state">Encoding state.</param>
@@ -4464,6 +4546,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
(mainFilters, subFilters, overlayFilters) = GetAmdVidFilterChain(state, options, outputVideoCodec);
}
+ else if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ (mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec);
+ }
else
{
(mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec);
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index b38ee1146..c8b29aa1f 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 8479b7d50..540d50bf1 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -56,6 +56,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libvpx",
"libvpx-vp9",
"aac",
+ "aac_at",
"libfdk_aac",
"ac3",
"libmp3lame",
@@ -106,7 +107,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
// vulkan
"libplacebo",
"scale_vulkan",
- "overlay_vulkan"
+ "overlay_vulkan",
+ "hwupload_vaapi",
+ // videotoolbox
+ "yadif_videotoolbox"
};
private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
@@ -273,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (match.Success)
{
- if (Version.TryParse(match.Groups[1].Value, out var result))
+ if (Version.TryParse(match.Groups[1].ValueSpan, out var result))
{
return result;
}
@@ -323,8 +327,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
RegexOptions.Multiline))
{
var version = new Version(
- int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture),
- int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture));
+ int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture),
+ int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture));
map.Add(match.Groups["name"].Value, version);
}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 8157dc0c2..5a1871070 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net
// Type image
{ "image/jpeg", ".jpg" },
+ { "image/tiff", ".tiff" },
{ "image/x-png", ".png" },
+ { "image/x-icon", ".ico" },
// Type text
{ "text/plain", ".txt" },
@@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net
var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
return string.IsNullOrEmpty(extension) ? null : "." + extension;
}
+
+ public static bool IsImage(ReadOnlySpan<char> mimeType)
+ => mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs
index 13bebc479..5b55667e8 100644
--- a/MediaBrowser.Model/Tasks/ITaskManager.cs
+++ b/MediaBrowser.Model/Tasks/ITaskManager.cs
@@ -9,9 +9,9 @@ namespace MediaBrowser.Model.Tasks
{
public interface ITaskManager : IDisposable
{
- event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ event EventHandler<GenericEventArgs<IScheduledTaskWorker>>? TaskExecuting;
- event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+ event EventHandler<TaskCompletionEventArgs>? TaskCompleted;
/// <summary>
/// Gets the list of Scheduled Tasks.
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 3634d0705..cc7d57eb0 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Users
public UserPolicy()
{
IsHidden = true;
+ EnableCollectionManagement = false;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
@@ -35,6 +36,7 @@ namespace MediaBrowser.Model.Users
EnableSharedDeviceControl = true;
BlockedTags = Array.Empty<string>();
+ AllowedTags = Array.Empty<string>();
BlockUnratedItems = Array.Empty<UnratedItem>();
EnableUserPreferenceAccess = true;
@@ -73,6 +75,12 @@ namespace MediaBrowser.Model.Users
public bool IsHidden { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether this instance can manage collections.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool EnableCollectionManagement { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is disabled.
/// </summary>
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
@@ -86,6 +94,8 @@ namespace MediaBrowser.Model.Users
public string[] BlockedTags { get; set; }
+ public string[] AllowedTags { get; set; }
+
public bool EnableUserPreferenceAccess { get; set; }
public AccessSchedule[] AccessSchedules { get; set; }
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index d621555f1..5d59c4663 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -313,7 +313,8 @@ namespace MediaBrowser.Providers.Manager
}
minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
- await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
+ var listWithNoLangFirst = list.OrderByDescending(i => string.IsNullOrEmpty(i.Language));
+ await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, listWithNoLangFirst, minWidth, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index ffae77200..9f287766a 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -334,6 +334,12 @@ namespace MediaBrowser.Providers.Manager
updateType |= UpdateCumulativeRunTimeTicks(item, children);
updateType |= UpdateDateLastMediaAdded(item, children);
+ // don't update user-changeable metadata for locked items
+ if (item.IsLocked)
+ {
+ return updateType;
+ }
+
if (EnableUpdatingPremiereDateFromChildren)
{
updateType |= UpdatePremiereDate(item, children);
@@ -375,7 +381,7 @@ namespace MediaBrowser.Providers.Manager
if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
{
folder.RunTimeTicks = ticks;
- return ItemUpdateType.MetadataEdit;
+ return ItemUpdateType.MetadataImport;
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 74210b1f2..19b594c1c 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -105,7 +105,10 @@ namespace MediaBrowser.Providers.MediaInfo
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
audio.Size = mediaInfo.Size;
- FetchDataFromTags(audio);
+ if (!audio.IsLocked)
+ {
+ FetchDataFromTags(audio);
+ }
_itemRepo.SaveMediaStreams(audio.Id, mediaInfo.MediaStreams, cancellationToken);
}
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 58cd23aa3..3476e7000 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -54,6 +54,12 @@ namespace MediaBrowser.Providers.Music
{
var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
+ // don't update user-changeable metadata for locked items
+ if (item.IsLocked)
+ {
+ return updateType;
+ }
+
if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
{
if (!item.LockedFields.Contains(MetadataField.Name))
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
index 22229e377..a2f3c63f0 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
@@ -1,5 +1,4 @@
using MediaBrowser.Model.Plugins;
-using MetaBrainz.MusicBrainz;
namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
@@ -8,7 +7,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
- private const string DefaultServer = "musicbrainz.org";
+ private const string DefaultServer = "https://musicbrainz.org";
private const double DefaultRateLimit = 1.0;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
index 6f1296bb7..62d86cd8f 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html
@@ -1,20 +1,16 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>MusicBrainz</title>
-</head>
-<body>
- <div data-role="page" class="page type-interior pluginConfigurationPage musicBrainzConfigPage" data-require="emby-input,emby-button,emby-checkbox">
- <div data-role="content">
- <div class="content-primary">
- <form class="musicBrainzConfigForm">
+<div id="musicBrainzConfigurationPage" data-role="page"
+ class="page type-interior pluginConfigurationPage musicBrainzConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div data-role="content">
+ <div class="content-primary">
+ <h1>MusicBrainz</h1>
+ <form class="musicBrainzConfigurationForm">
<div class="inputContainer">
<input is="emby-input" type="text" id="server" required label="Server" />
<div class="fieldDescription">This can be a mirror of the official server or even a custom server.</div>
</div>
<div class="inputContainer">
- <input is="emby-input" type="number" id="rateLimit" pattern="[0-9]*" required min="0" max="10000" label="Rate Limit" />
- <div class="fieldDescription">Span of time between requests in milliseconds. The official server is limited to one request every two seconds.</div>
+ <input is="emby-input" type="number" id="rateLimit" required pattern="[0-9]*" min="0" max="10" step=".01" label="Rate Limit" />
+ <div class="fieldDescription">Span of time between requests in seconds. The official server is limited to one request every seconds.</div>
</div>
<label class="checkboxContainer">
<input is="emby-checkbox" type="checkbox" id="replaceArtistName" />
@@ -32,7 +28,7 @@
uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a"
};
- document.querySelector('.musicBrainzConfigPage')
+ document.querySelector('.musicBrainzConfigurationPage')
.addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
@@ -49,14 +45,14 @@
bubbles: true,
cancelable: false
}));
-
+
document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName;
Dashboard.hideLoadingMsg();
});
});
- document.querySelector('.musicBrainzConfigForm')
+ document.querySelector('.musicBrainzConfigurationForm')
.addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 34f45f0d5..3afa90bae 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -8,8 +8,10 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Music;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
using MetaBrainz.MusicBrainz;
using MetaBrainz.MusicBrainz.Interfaces.Entities;
using MetaBrainz.MusicBrainz.Interfaces.Searches;
@@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
{
private readonly ILogger<MusicBrainzAlbumProvider> _logger;
- private readonly Query _musicBrainzQuery;
- private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org";
+ private Query _musicBrainzQuery;
/// <summary>
/// Initializes a new instance of the <see cref="MusicBrainzAlbumProvider"/> class.
@@ -33,29 +34,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
public MusicBrainzAlbumProvider(ILogger<MusicBrainzAlbumProvider> logger)
{
_logger = logger;
-
- MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
- {
- if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server))
- {
- Query.DefaultServer = server.Host;
- Query.DefaultPort = server.Port;
- Query.DefaultUrlScheme = server.Scheme;
- }
- else
- {
- // Fallback to official server
- _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
- var defaultServer = new Uri(_musicBrainzDefaultUri);
- Query.DefaultServer = defaultServer.Host;
- Query.DefaultPort = defaultServer.Port;
- Query.DefaultUrlScheme = defaultServer.Scheme;
- }
-
- Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
- };
-
_musicBrainzQuery = new Query();
+ ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
+ MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
}
/// <inheritdoc />
@@ -64,6 +45,29 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
/// <inheritdoc />
public int Order => 0;
+ private void ReloadConfig(object? sender, BasePluginConfiguration e)
+ {
+ var configuration = (PluginConfiguration)e;
+ if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
+ {
+ Query.DefaultServer = server.DnsSafeHost;
+ Query.DefaultPort = server.Port;
+ Query.DefaultUrlScheme = server.Scheme;
+ }
+ else
+ {
+ // Fallback to official server
+ _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
+ var defaultServer = new Uri(configuration.Server);
+ Query.DefaultServer = defaultServer.Host;
+ Query.DefaultPort = defaultServer.Port;
+ Query.DefaultUrlScheme = defaultServer.Scheme;
+ }
+
+ Query.DelayBetweenRequests = configuration.RateLimit;
+ _musicBrainzQuery = new Query();
+ }
+
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
{
@@ -72,13 +76,13 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
if (!string.IsNullOrEmpty(releaseId))
{
- var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
+ var releaseResult = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
return GetReleaseResult(releaseResult).SingleItemAsEnumerable();
}
if (!string.IsNullOrEmpty(releaseGroupId))
{
- var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.None, null, cancellationToken).ConfigureAwait(false);
+ var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false);
return GetReleaseGroupResult(releaseGroupResult.Releases);
}
@@ -133,7 +137,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
foreach (var result in releaseSearchResults)
{
- yield return GetReleaseResult(result);
+ // Fetch full release info, otherwise artists are missing
+ var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups);
+ yield return GetReleaseResult(fullResult);
}
}
@@ -143,21 +149,33 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
{
Name = releaseSearchResult.Title,
ProductionYear = releaseSearchResult.Date?.Year,
- PremiereDate = releaseSearchResult.Date?.NearestDate
+ PremiereDate = releaseSearchResult.Date?.NearestDate,
+ SearchProviderName = Name
};
- if (releaseSearchResult.ArtistCredit?.Count > 0)
+ // Add artists and use first as album artist
+ var artists = releaseSearchResult.ArtistCredit;
+ if (artists is not null && artists.Count > 0)
{
- searchResult.AlbumArtist = new RemoteSearchResult
- {
- SearchProviderName = Name,
- Name = releaseSearchResult.ArtistCredit[0].Name
- };
+ var artistResults = new List<RemoteSearchResult>();
- if (releaseSearchResult.ArtistCredit[0].Artist?.Id is not null)
+ foreach (var artist in artists)
{
- searchResult.AlbumArtist.SetProviderId(MetadataProvider.MusicBrainzArtist, releaseSearchResult.ArtistCredit[0].Artist!.Id.ToString());
+ var artistResult = new RemoteSearchResult
+ {
+ Name = artist.Name
+ };
+
+ if (artist.Artist?.Id is not null)
+ {
+ artistResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Artist!.Id.ToString());
+ }
+
+ artistResults.Add(artistResult);
}
+
+ searchResult.AlbumArtist = artistResults[0];
+ searchResult.Artists = artistResults.ToArray();
}
searchResult.SetProviderId(MetadataProvider.MusicBrainzAlbum, releaseSearchResult.Id.ToString());
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
index 718b5a1c4..be1d87675 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
@@ -8,8 +8,10 @@ using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Music;
+using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
using MetaBrainz.MusicBrainz;
using MetaBrainz.MusicBrainz.Interfaces.Entities;
using MetaBrainz.MusicBrainz.Interfaces.Searches;
@@ -23,8 +25,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
{
private readonly ILogger<MusicBrainzArtistProvider> _logger;
- private readonly Query _musicBrainzQuery;
- private readonly string _musicBrainzDefaultUri = "https://musicbrainz.org";
+ private Query _musicBrainzQuery;
/// <summary>
/// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class.
@@ -33,34 +34,37 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
public MusicBrainzArtistProvider(ILogger<MusicBrainzArtistProvider> logger)
{
_logger = logger;
-
- MusicBrainz.Plugin.Instance!.ConfigurationChanged += (_, _) =>
- {
- if (Uri.TryCreate(MusicBrainz.Plugin.Instance.Configuration.Server, UriKind.Absolute, out var server))
- {
- Query.DefaultServer = server.Host;
- Query.DefaultPort = server.Port;
- Query.DefaultUrlScheme = server.Scheme;
- }
- else
- {
- // Fallback to official server
- _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
- var defaultServer = new Uri(_musicBrainzDefaultUri);
- Query.DefaultServer = defaultServer.Host;
- Query.DefaultPort = defaultServer.Port;
- Query.DefaultUrlScheme = defaultServer.Scheme;
- }
-
- Query.DelayBetweenRequests = MusicBrainz.Plugin.Instance.Configuration.RateLimit;
- };
-
_musicBrainzQuery = new Query();
+ ReloadConfig(null, MusicBrainz.Plugin.Instance!.Configuration);
+ MusicBrainz.Plugin.Instance!.ConfigurationChanged += ReloadConfig;
}
/// <inheritdoc />
public string Name => "MusicBrainz";
+ private void ReloadConfig(object? sender, BasePluginConfiguration e)
+ {
+ var configuration = (PluginConfiguration)e;
+ if (Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server))
+ {
+ Query.DefaultServer = server.DnsSafeHost;
+ Query.DefaultPort = server.Port;
+ Query.DefaultUrlScheme = server.Scheme;
+ }
+ else
+ {
+ // Fallback to official server
+ _logger.LogWarning("Invalid MusicBrainz server specified, falling back to official server");
+ var defaultServer = new Uri(configuration.Server);
+ Query.DefaultServer = defaultServer.Host;
+ Query.DefaultPort = defaultServer.Port;
+ Query.DefaultUrlScheme = defaultServer.Scheme;
+ }
+
+ Query.DelayBetweenRequests = configuration.RateLimit;
+ _musicBrainzQuery = new Query();
+ }
+
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
{
@@ -112,7 +116,8 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
{
Name = artist.Name,
ProductionYear = artist.LifeSpan?.Begin?.Year,
- PremiereDate = artist.LifeSpan?.Begin?.NearestDate
+ PremiereDate = artist.LifeSpan?.Begin?.NearestDate,
+ SearchProviderName = Name,
};
searchResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Id.ToString());
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
index ac3df1d5d..450ee2a33 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
@@ -11,7 +11,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// The TMDb API controller.
/// </summary>
[ApiController]
- [Authorize(Policy = "DefaultAuthorization")]
+ [Authorize]
[Route("[controller]")]
[Produces(MediaTypeNames.Application.Json)]
public class TmdbController : ControllerBase
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 12570f0a7..d89fb814d 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -193,43 +193,43 @@ namespace MediaBrowser.Providers.Subtitles
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
}
- }
- var savePaths = new List<string>();
- var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
+ var savePaths = new List<string>();
+ var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
- if (response.IsForced)
- {
- saveFileName += ".forced";
- }
+ if (response.IsForced)
+ {
+ saveFileName += ".forced";
+ }
- saveFileName += "." + response.Format.ToLowerInvariant();
+ saveFileName += "." + response.Format.ToLowerInvariant();
- if (saveInMediaFolder)
- {
- var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
- // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
- if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal))
+ if (saveInMediaFolder)
{
- savePaths.Add(mediaFolderPath);
+ var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
+ // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
+ if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal))
+ {
+ savePaths.Add(mediaFolderPath);
+ }
}
- }
- var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
+ var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
- // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
- if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
- {
- savePaths.Add(internalPath);
- }
+ // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
+ if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
+ {
+ savePaths.Add(internalPath);
+ }
- if (savePaths.Count > 0)
- {
- await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
- }
- else
- {
- _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
+ if (savePaths.Count > 0)
+ {
+ await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
+ }
+ else
+ {
+ _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
+ }
}
}
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index e02087a52..95b08eb05 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 6962b6bc1..18fb7bebe 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 96e3ca403..e0555cd22 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index f1c536399..ad5a0890b 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index eaea305d1..2d8be1835 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c646b288-5d5b-4c9c-a95b-e1fad1c0d95d/e13d71d48b629fe3a85f5676deb09e2d/dotnet-sdk-7.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/794cd64a-31ac-4070-ac39-34858e8c00da/9568dfe47bd2d22de99268ceac5b2bef/dotnet-sdk-7.0.103-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/nuget.config b/nuget.config
deleted file mode 100644
index 326331f32..000000000
--- a/nuget.config
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
- <packageSources>
- <add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
- </packageSources>
-</configuration>
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 15261bb65..4f80aa941 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -33,7 +33,7 @@
</ItemGroup>
<!-- Code Analyzers-->
- <ItemGroup>
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index f489d6fd0..3f4f55ee4 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<!-- Code Analyzers-->
- <ItemGroup>
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 3801a1cc3..71572bcf6 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -10,7 +10,7 @@
</ItemGroup>
<!-- Code Analyzers-->
- <ItemGroup>
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
new file mode 100644
index 000000000..de8fc1bb8
--- /dev/null
+++ b/tests/Directory.Build.props
@@ -0,0 +1,23 @@
+<Project>
+ <!-- Sets defaults for all test projects -->
+
+ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup>
+ <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
index ee42216e4..6669a6689 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
@@ -2,7 +2,8 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
@@ -11,25 +12,25 @@ using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
-namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
+namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
{
- public class FirstTimeSetupOrElevatedHandlerTests
+ public class FirstTimeSetupHandlerTests
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
- private readonly FirstTimeSetupOrElevatedHandler _sut;
+ private readonly FirstTimeSetupHandler _firstTimeSetupHandler;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
- public FirstTimeSetupOrElevatedHandlerTests()
+ public FirstTimeSetupHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
+ _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
- _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
+ _firstTimeSetupHandler = fixture.Create<FirstTimeSetupHandler>();
}
[Theory]
@@ -46,7 +47,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
var context = new AuthorizationHandlerContext(_requirements, claims, null);
- await _sut.HandleAsync(context);
+ await _firstTimeSetupHandler.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
var context = new AuthorizationHandlerContext(_requirements, claims, null);
- await _sut.HandleAsync(context);
+ await _firstTimeSetupHandler.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
index 7150c90bb..9cf8f8548 100644
--- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
- private readonly IgnoreParentalControlHandler _sut;
+ private readonly DefaultAuthorizationHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
@@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new IgnoreParentalControlRequirement() };
+ _requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement(validateParentalSchedule: false) };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
- _sut = fixture.Create<IgnoreParentalControlHandler>();
+ _sut = fixture.Create<DefaultAuthorizationHandler>();
}
[Theory]
diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
deleted file mode 100644
index 5b3d784ff..000000000
--- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System.Collections.Generic;
-using System.Net;
-using System.Threading.Tasks;
-using AutoFixture;
-using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.LocalAccessPolicy;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
-{
- public class LocalAccessHandlerTests
- {
- private readonly Mock<IConfigurationManager> _configurationManagerMock;
- private readonly List<IAuthorizationRequirement> _requirements;
- private readonly LocalAccessHandler _sut;
- private readonly Mock<IUserManager> _userManagerMock;
- private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
- private readonly Mock<INetworkManager> _networkManagerMock;
-
- public LocalAccessHandlerTests()
- {
- var fixture = new Fixture().Customize(new AutoMoqCustomization());
- _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() };
- _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
- _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
- _networkManagerMock = fixture.Freeze<Mock<INetworkManager>>();
-
- _sut = fixture.Create<LocalAccessHandler>();
- }
-
- [Theory]
- [InlineData(true, true)]
- [InlineData(false, false)]
- public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
- {
- _networkManagerMock
- .Setup(n => n.IsInLocalNetwork(It.IsAny<IPAddress>()))
- .Returns(isInLocalNetwork);
-
- TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
- var claims = TestHelpers.SetupUser(
- _userManagerMock,
- _httpContextAccessor,
- UserRoles.User);
-
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
- await _sut.HandleAsync(context);
- Assert.Equal(shouldSucceed, context.HasSucceeded);
- }
- }
-}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
deleted file mode 100644
index ffe88fcde..000000000
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using AutoFixture;
-using AutoFixture.AutoMoq;
-using Jellyfin.Api.Auth.RequiresElevationPolicy;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Library;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Moq;
-using Xunit;
-
-namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
-{
- public class RequiresElevationHandlerTests
- {
- private readonly Mock<IConfigurationManager> _configurationManagerMock;
- private readonly List<IAuthorizationRequirement> _requirements;
- private readonly RequiresElevationHandler _sut;
- private readonly Mock<IUserManager> _userManagerMock;
- private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
-
- public RequiresElevationHandlerTests()
- {
- var fixture = new Fixture().Customize(new AutoMoqCustomization());
- _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
- _requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
- _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
- _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
-
- _sut = fixture.Create<RequiresElevationHandler>();
- }
-
- [Theory]
- [InlineData(UserRoles.Administrator, true)]
- [InlineData(UserRoles.User, false)]
- [InlineData(UserRoles.Guest, false)]
- public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
- {
- TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
- var claims = TestHelpers.SetupUser(
- _userManagerMock,
- _httpContextAccessor,
- role);
-
- var context = new AuthorizationHandlerContext(_requirements, claims, null);
-
- await _sut.HandleAsync(context);
- Assert.Equal(shouldSucceed, context.HasSucceeded);
- }
- }
-}
diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs
new file mode 100644
index 000000000..d6428fb2c
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs
@@ -0,0 +1,36 @@
+using System;
+using Jellyfin.Api.Controllers;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Controllers;
+
+public static class ImageControllerTests
+{
+ [Theory]
+ [InlineData("image/apng", ".apng")]
+ [InlineData("image/avif", ".avif")]
+ [InlineData("image/bmp", ".bmp")]
+ [InlineData("image/gif", ".gif")]
+ [InlineData("image/x-icon", ".ico")]
+ [InlineData("image/jpeg", ".jpg")]
+ [InlineData("image/png", ".png")]
+ [InlineData("image/png; charset=utf-8", ".png")]
+ [InlineData("image/svg+xml", ".svg")]
+ [InlineData("image/tiff", ".tiff")]
+ [InlineData("image/webp", ".webp")]
+ public static void TryGetImageExtensionFromContentType_Valid_True(string contentType, string extension)
+ {
+ Assert.True(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
+ Assert.Equal(extension, ex);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("text/html")]
+ public static void TryGetImageExtensionFromContentType_InValid_False(string contentType)
+ {
+ Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
+ Assert.Null(ex);
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index c4640bd22..2d7741d81 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.Security.Claims;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Net;
using Xunit;
namespace Jellyfin.Api.Tests.Helpers
@@ -15,6 +19,82 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
+ [Fact]
+ public static void GetUserId_IsAdmin()
+ {
+ Guid? requestUserId = Guid.NewGuid();
+ Guid? authUserId = Guid.NewGuid();
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.IsApiKey, bool.FalseString),
+ new Claim(ClaimTypes.Role, UserRoles.Administrator)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ var userId = RequestHelpers.GetUserId(principal, requestUserId);
+
+ Assert.Equal(requestUserId, userId);
+ }
+
+ [Fact]
+ public static void GetUserId_IsApiKey_EmptyGuid()
+ {
+ Guid? requestUserId = Guid.Empty;
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ var userId = RequestHelpers.GetUserId(principal, requestUserId);
+
+ Assert.Equal(Guid.Empty, userId);
+ }
+
+ [Fact]
+ public static void GetUserId_IsApiKey_Null()
+ {
+ Guid? requestUserId = null;
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.IsApiKey, bool.TrueString)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ var userId = RequestHelpers.GetUserId(principal, requestUserId);
+
+ Assert.Equal(Guid.Empty, userId);
+ }
+
+ [Fact]
+ public static void GetUserId_IsUser()
+ {
+ Guid? requestUserId = Guid.NewGuid();
+ Guid? authUserId = Guid.NewGuid();
+
+ var claims = new[]
+ {
+ new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.IsApiKey, bool.FalseString),
+ new Claim(ClaimTypes.Role, UserRoles.User)
+ };
+
+ var identity = new ClaimsIdentity(claims, string.Empty);
+ var principal = new ClaimsPrincipal(identity);
+
+ Assert.Throws<SecurityException>(() => RequestHelpers.GetUserId(principal, requestUserId));
+ }
+
public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
{
var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 6202f83dc..015018910 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="AutoFixture" />
<PackageReference Include="AutoFixture.AutoMoq" />
@@ -27,17 +21,6 @@
<PackageReference Include="Moq" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
<ProjectReference Include="../../Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 699c12217..8fef7fde0 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
@@ -22,17 +16,6 @@
<PackageReference Include="FsCheck.Xunit" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 1e729a46a..54d93b48c 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
@@ -22,17 +16,6 @@
<PackageReference Include="coverlet.collector" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 2be5da2c2..69677ce42 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
@@ -17,17 +11,6 @@
<PackageReference Include="coverlet.collector" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../Emby.Dlna/Emby.Dlna.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index dbbb61cc4..036489829 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
@@ -20,17 +14,6 @@
<PackageReference Include="FsCheck.Xunit" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
<ProjectReference Include="../../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" />
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index 10c141873..eab003715 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
@@ -19,16 +13,6 @@
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup>
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
<ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Keyframes\Jellyfin.MediaEncoding.Keyframes.csproj" />
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index 4910a041a..894bec6aa 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -1,12 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- <RootNamespace>Jellyfin.MediaEncoding.Keyframes</RootNamespace>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
@@ -20,17 +13,6 @@
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 077466b6e..6b703e741 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -31,17 +25,6 @@
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index cffd7bc0b..8345b610e 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
@@ -24,17 +18,6 @@
</None>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
index cbab455f0..371c3811a 100644
--- a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
+++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
@@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net
[InlineData("image/jpeg", ".jpg")]
[InlineData("image/png", ".png")]
[InlineData("image/svg+xml", ".svg")]
- [InlineData("image/tiff", ".tif")]
+ [InlineData("image/tiff", ".tiff")]
[InlineData("image/vnd.microsoft.icon", ".ico")]
[InlineData("image/webp", ".webp")]
+ [InlineData("image/x-icon", ".ico")]
[InlineData("image/x-png", ".png")]
[InlineData("text/css", ".css")]
[InlineData("text/csv", ".csv")]
diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
index 58aaed023..c49663248 100644
--- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
+++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
@@ -12,8 +12,6 @@ namespace Jellyfin.Naming.Tests.Common
Assert.NotEmpty(options.CleanDateTimeRegexes);
Assert.NotEmpty(options.CleanStringRegexes);
- Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
- Assert.NotEmpty(options.EpisodeMultiPartRegexes);
}
[Fact]
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index c5e93f0bb..112dd780e 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
@@ -26,15 +20,4 @@
<ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
</Project>
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
index 68059f980..406381f14 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
@@ -73,6 +73,11 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number
[InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number
[InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
+ [InlineData("Season 3/The Series Season 3 Episode 9 - The title.avi", 9)]
+ [InlineData("Season 3/The Series S3 E9 - The title.avi", 9)]
+ [InlineData("Season 3/S003 E009.avi", 9)]
+ [InlineData("Season 3/Season 3 Episode 9.avi", 9)]
+
// [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
index af219b118..7604ddc80 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs
@@ -30,6 +30,7 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)]
[InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)]
[InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)]
+ [InlineData("/The.Sopranos/Season 3/The Sopranos Season 3 Episode 09 - The Telltale Moozadell.avi", false, "The Sopranos", 3, 9)]
// TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)]
// TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)]
// TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)]
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 287d881a8..02e6f6368 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -324,6 +324,25 @@ namespace Jellyfin.Naming.Tests.Video
}
[Fact]
+ public void TestMultiVersion12()
+ {
+ var files = new[]
+ {
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv",
+ @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv"
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ _namingOptions).ToList();
+
+ Assert.Single(result);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[0].Path);
+ }
+
+ [Fact]
public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
{
var files = new[]
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index e24569926..4b4bdd2a5 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -5,12 +5,6 @@
<ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
@@ -23,17 +17,6 @@
<PackageReference Include="Moq" />
</ItemGroup>
- <!-- Code Analyzers-->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../Emby.Server.Implementations/Emby.Server.Implementations.csproj" />
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
index 61f913252..df2a2ca70 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -45,6 +45,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("fd23:184f:2029:0::/56", "fd24:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0::/56, !fd23:184f:2029:0:3139:7386:67d7:d500/120", "fd23:184f:2029:0:3139:7386:67d7:d517")]
[InlineData("fd23:184f:2029:0::/56", "192.168.10.60")]
+ [InlineData("2001:abcd:abcd:6b40::0/60", "192.168.10.60")]
public void InNetwork_False_Success(string network, string value)
{
var ip = IPAddress.Parse(value);
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 27151c847..c12f0cd68 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -26,17 +20,6 @@
</PackageReference>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />
</ItemGroup>
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 2150520e5..9b6cb40b0 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -5,13 +5,6 @@
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
</PropertyGroup>
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- <RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -32,17 +25,6 @@
<PackageReference Include="coverlet.collector" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="..\..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
index 82ce8fc4e..92b4178fd 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
@@ -67,4 +67,23 @@ public class XmlTvListingsProviderTests
Assert.Equal("https://domain.tld/image.png", program.ImageUrl);
Assert.Equal("3297", program.ChannelId);
}
+
+ [Theory]
+ [InlineData("Test Data/LiveTv/Listings/XmlTv/emptycategory.xml")]
+ [InlineData("https://example.com/emptycategory.xml")]
+ public async Task GetProgramsAsync_EmptyCategories_Success(string path)
+ {
+ var info = new ListingsProviderInfo()
+ {
+ Path = path
+ };
+
+ var startDate = new DateTime(2022, 11, 4);
+ var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None);
+ var programsList = programs.ToList();
+ Assert.Single(programsList);
+ var program = programsList[0];
+ Assert.DoesNotContain(program.Genres, g => string.IsNullOrEmpty(g));
+ Assert.Equal("3297", program.ChannelId);
+ }
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
new file mode 100644
index 000000000..dd4aa8977
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
@@ -0,0 +1,6 @@
+<tv date="20221104">
+ <programme channel="3297" start="20221104130000 -0400" stop="20221105235959 -0400">
+ <category lang="en" />
+ <category lang="en">sports</category>
+ </programme>
+</tv>
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 9eb0beda4..3737fee0a 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
using Xunit;
namespace Jellyfin.Server.Integration.Tests
@@ -43,6 +44,33 @@ namespace Jellyfin.Server.Integration.Tests
return auth!.AccessToken;
}
+ public static async Task<UserDto> GetUserDtoAsync(HttpClient client)
+ {
+ using var response = await client.GetAsync("Users/Me").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var userDto = await JsonSerializer.DeserializeAsync<UserDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false), JsonDefaults.Options).ConfigureAwait(false);
+ Assert.NotNull(userDto);
+ return userDto;
+ }
+
+ public static async Task<BaseItemDto> GetRootFolderDtoAsync(HttpClient client, Guid userId = default)
+ {
+ if (userId.Equals(default))
+ {
+ var userDto = await GetUserDtoAsync(client).ConfigureAwait(false);
+ userId = userDto.Id;
+ }
+
+ var response = await client.GetAsync($"Users/{userId}/Items/Root").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ JsonDefaults.Options).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ return rootDto;
+ }
+
public static void AddAuthHeader(this HttpHeaders headers, string accessToken)
{
headers.Add(AuthHeaderName, DummyAuthHeader + $", Token={accessToken}");
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
new file mode 100644
index 000000000..078002994
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public ItemsControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetItems_NoApiKeyOrUserId_Success()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("Items").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items")]
+ [InlineData("Users/{0}/Items/Resume")]
+ public async Task GetUserItems_NonExistentUserId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("Items?userId={0}")]
+ [InlineData("Users/{0}/Items")]
+ [InlineData("Users/{0}/Items/Resume")]
+ public async Task GetItems_UserId_Ok(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(items);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
new file mode 100644
index 000000000..013d19a9f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public LibraryControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Theory]
+ [InlineData("Items/{0}/File")]
+ [InlineData("Items/{0}/ThemeSongs")]
+ [InlineData("Items/{0}/ThemeVideos")]
+ [InlineData("Items/{0}/ThemeMedia")]
+ [InlineData("Items/{0}/Ancestors")]
+ [InlineData("Items/{0}/Download")]
+ [InlineData("Artists/{0}/Similar")]
+ [InlineData("Items/{0}/Similar")]
+ [InlineData("Albums/{0}/Similar")]
+ [InlineData("Shows/{0}/Similar")]
+ [InlineData("Movies/{0}/Similar")]
+ [InlineData("Trailers/{0}/Similar")]
+ public async Task Get_NonExistentItemId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
new file mode 100644
index 000000000..17f3dc99f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs
@@ -0,0 +1,26 @@
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public MusicGenreControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task MusicGenres_FakeMusicGenre_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("MusicGenres/Fake-MusicGenre").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
index f8f5fecec..868ecd53f 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
@@ -1,18 +1,13 @@
using System;
using System.Net;
-using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
-using Xunit.Priority;
namespace Jellyfin.Server.Integration.Tests.Controllers;
-[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
- private static readonly Guid _testUserId = Guid.NewGuid();
- private static readonly Guid _testItemId = Guid.NewGuid();
private static string? _accessToken;
public PlaystateControllerTests(JellyfinApplicationFactory factory)
@@ -20,31 +15,47 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
_factory = factory;
}
- private Task<HttpResponseMessage> DeleteUserPlayedItems(HttpClient httpClient, Guid userId, Guid itemId)
- => httpClient.DeleteAsync($"Users/{userId}/PlayedItems/{itemId}");
+ [Fact]
+ public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task PostMarkPlayedItem_NonExistentUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
- private Task<HttpResponseMessage> PostUserPlayedItems(HttpClient httpClient, Guid userId, Guid itemId)
- => httpClient.PostAsync($"Users/{userId}/PlayedItems/{itemId}", null);
+ using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
[Fact]
- [Priority(0)]
- public async Task DeleteMarkUnplayedItem_DoesNotExist_NotFound()
+ public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
- using var response = await DeleteUserPlayedItems(client, _testUserId, _testItemId).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}").ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
- [Priority(0)]
- public async Task PostMarkPlayedItem_DoesNotExist_NotFound()
+ public async Task PostMarkPlayedItem_NonExistentItemId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
- using var response = await PostUserPlayedItems(client, _testUserId, _testItemId).ConfigureAwait(false);
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
new file mode 100644
index 000000000..cb0a829e8
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public class SessionControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public SessionControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetSessions_NonExistentUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.GetAsync($"Session/Sessions?userId={Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
index 2b825a93a..2a3c53dbe 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -67,6 +67,16 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
}
[Fact]
+ [Priority(-1)]
+ public async Task Me_Valid_Success()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ _ = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ }
+
+ [Fact]
[Priority(0)]
public async Task New_Valid_Success()
{
@@ -108,7 +118,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
var createRequest = new CreateUserByName()
{
- Name = username
+ Name = username!
};
using var response = await CreateUserByName(client, createRequest).ConfigureAwait(false);
@@ -116,6 +126,19 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
}
[Fact]
+ [Priority(0)]
+ public async Task Delete_DoesntExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+
+ // access token can't be null here as the previous test populated it
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
+
+ using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
[Priority(1)]
public async Task UpdateUserPassword_Valid_Success()
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
new file mode 100644
index 000000000..69f2ccf33
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public UserLibraryControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetRootFolder_NonExistenUserId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetRootFolder_UserId_Valid()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ _ = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items/{1}")]
+ [InlineData("Users/{0}/Items/{1}/Intros")]
+ [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+ [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+ [InlineData("Users/{0}/Items/{1}/Lyrics")]
+ public async Task GetItem_NonExistenUserId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items/{1}")]
+ [InlineData("Users/{0}/Items/{1}/Intros")]
+ [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+ [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+ [InlineData("Users/{0}/Items/{1}/Lyrics")]
+ public async Task GetItem_NonExistentItemId_NotFound(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetItem_UserIdAndItemId_Valid()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+ var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ }
+
+ [Fact]
+ public async Task GetIntros_UserIdAndItemId_Valid()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+ var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ }
+
+ [Theory]
+ [InlineData("Users/{0}/Items/{1}/LocalTrailers")]
+ [InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
+ public async Task LocalTrailersAndSpecialFeatures_UserIdAndItemId_Valid(string format)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var userDto = await AuthHelper.GetUserDtoAsync(client).ConfigureAwait(false);
+ var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id).ConfigureAwait(false);
+
+ var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+ Assert.NotNull(rootDto);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
new file mode 100644
index 000000000..0f9a2e90a
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public VideosControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task DeleteAlternateSources_NonExistentItemId_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
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 26b2cd239..a5296d8c9 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -1,9 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" />
@@ -29,17 +24,6 @@
</None>
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index d47f70cff..5fea805ae 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<PackageReference Include="AutoFixture" />
<PackageReference Include="AutoFixture.AutoMoq" />
@@ -22,17 +16,6 @@
<PackageReference Include="Moq" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../Jellyfin.Server/Jellyfin.Server.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index fb7864cd1..9fe0744de 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -1,11 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<None Include="Test Data\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -23,17 +17,6 @@
<PackageReference Include="coverlet.collector" />
</ItemGroup>
- <!-- Code Analyzers -->
- <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
- <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
- <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
- <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
- </ItemGroup>
-
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="../../MediaBrowser.Providers/MediaBrowser.Providers.csproj" />