aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml93
-rw-r--r--.ci/azure-pipelines-main.yml71
-rw-r--r--.ci/azure-pipelines-package.yml274
-rw-r--r--.ci/azure-pipelines-test.yml98
-rw-r--r--.ci/azure-pipelines.yml64
-rw-r--r--.config/dotnet-tools.json2
l---------.copr1
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml59
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--.github/workflows/commands.yml25
-rw-r--r--.github/workflows/issue-template-check.yml29
-rw-r--r--CONTRIBUTORS.md5
-rw-r--r--Directory.Packages.props51
-rw-r--r--Dockerfile87
-rw-r--r--Dockerfile.arm74
-rw-r--r--Dockerfile.arm6474
-rw-r--r--Emby.Naming/Common/NamingOptions.cs6
-rw-r--r--Emby.Photos/PhotoProvider.cs241
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs16
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs2
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs5
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs32
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs3
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs14
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs17
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs20
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs11
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs35
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs10
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs23
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs6
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs39
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs5
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs9
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs10
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/my.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json4
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs174
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs31
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs40
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs18
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs5
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs101
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs6
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs303
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs8
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs18
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs8
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs15
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs34
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs12
-rw-r--r--Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs4
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs13
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs21
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs36
-rw-r--r--Jellyfin.Data/Enums/MediaStreamProtocol.cs8
-rw-r--r--Jellyfin.Data/Enums/VideoRangeType.cs17
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs1
-rw-r--r--Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs132
-rw-r--r--Jellyfin.Server/Helpers/StartupHelpers.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs6
-rw-r--r--Jellyfin.Server/Program.cs11
-rw-r--r--MediaBrowser.Controller/Channels/IHasCacheKey.cs4
-rw-r--r--MediaBrowser.Controller/Channels/ISearchableChannel.cs21
-rw-r--r--MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs4
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs24
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs11
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs49
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs2
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs8
-rw-r--r--MediaBrowser.Controller/LiveTv/ChannelInfo.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs312
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs8
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs178
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs2
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs174
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs44
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs44
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs5
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs1
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs1
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs18
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs19
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs25
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs18
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs28
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs12
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs2
-rw-r--r--MediaBrowser.Model/Configuration/TrickplayOptions.cs5
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs14
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs10
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs2
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs2
-rw-r--r--MediaBrowser.Model/Entities/CollectionTypeOptions.cs59
-rw-r--r--MediaBrowser.Model/Entities/IHasShares.cs6
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs46
-rw-r--r--MediaBrowser.Model/Entities/PlaylistUserPermissions.cs30
-rw-r--r--MediaBrowser.Model/Entities/Share.cs17
-rw-r--r--MediaBrowser.Model/Entities/VirtualFolderInfo.cs1
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs11
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs41
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs24
-rw-r--r--MediaBrowser.Model/Search/SearchQuery.cs3
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs4
-rw-r--r--MediaBrowser.Model/System/LogFile.cs3
-rw-r--r--MediaBrowser.Model/Tasks/ITaskTrigger.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs3
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs20
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs35
l---------build1
-rwxr-xr-xbuild.sh114
-rw-r--r--build.yaml18
-rwxr-xr-xbump_version78
-rw-r--r--debian/changelog89
-rw-r--r--debian/compat1
-rw-r--r--debian/conf/jellyfin53
-rw-r--r--debian/conf/jellyfin.service.conf55
-rw-r--r--debian/conf/logging.json30
-rw-r--r--debian/control27
-rw-r--r--debian/copyright29
-rw-r--r--debian/gbp.conf6
-rw-r--r--debian/install4
-rw-r--r--debian/jellyfin.init62
-rw-r--r--debian/jellyfin.service17
-rw-r--r--debian/jellyfin.upstart20
-rw-r--r--debian/metapackage/jellyfin13
-rw-r--r--debian/po/POTFILES.in1
-rw-r--r--debian/po/templates.pot57
-rw-r--r--debian/postinst102
-rw-r--r--debian/postrm81
-rw-r--r--debian/preinst78
-rw-r--r--debian/prerm61
-rwxr-xr-xdebian/rules55
-rw-r--r--debian/source.lintian-overrides3
-rw-r--r--debian/source/format1
-rw-r--r--debian/source/options11
-rw-r--r--deployment/Dockerfile.centos.amd6439
-rw-r--r--deployment/Dockerfile.debian.amd6433
-rw-r--r--deployment/Dockerfile.debian.arm6446
-rw-r--r--deployment/Dockerfile.debian.armhf47
-rw-r--r--deployment/Dockerfile.docker.amd6412
-rw-r--r--deployment/Dockerfile.docker.arm6412
-rw-r--r--deployment/Dockerfile.docker.armhf12
-rw-r--r--deployment/Dockerfile.fedora.amd6439
-rw-r--r--deployment/Dockerfile.linux.amd6433
-rw-r--r--deployment/Dockerfile.linux.amd64-musl33
-rw-r--r--deployment/Dockerfile.linux.arm6433
-rw-r--r--deployment/Dockerfile.linux.armhf33
-rw-r--r--deployment/Dockerfile.linux.musl-linux-arm6433
-rw-r--r--deployment/Dockerfile.macos.amd6433
-rw-r--r--deployment/Dockerfile.macos.arm6433
-rw-r--r--deployment/Dockerfile.portable32
-rw-r--r--deployment/Dockerfile.ubuntu.amd6433
-rw-r--r--deployment/Dockerfile.ubuntu.arm6456
-rw-r--r--deployment/Dockerfile.ubuntu.armhf56
-rw-r--r--deployment/Dockerfile.windows.amd6432
-rwxr-xr-xdeployment/build.centos.amd6459
-rwxr-xr-xdeployment/build.debian.amd6437
-rwxr-xr-xdeployment/build.debian.arm6438
-rwxr-xr-xdeployment/build.debian.armhf38
-rwxr-xr-xdeployment/build.fedora.amd6459
-rwxr-xr-xdeployment/build.linux.amd6431
-rwxr-xr-xdeployment/build.linux.amd64-musl31
-rwxr-xr-xdeployment/build.linux.arm6431
-rwxr-xr-xdeployment/build.linux.armhf31
-rwxr-xr-xdeployment/build.linux.musl-linux-arm6431
-rwxr-xr-xdeployment/build.macos.amd6431
-rwxr-xr-xdeployment/build.macos.arm6431
-rwxr-xr-xdeployment/build.portable31
-rwxr-xr-xdeployment/build.ubuntu.amd6437
-rwxr-xr-xdeployment/build.ubuntu.arm6438
-rwxr-xr-xdeployment/build.ubuntu.armhf38
-rwxr-xr-xdeployment/build.windows.amd6452
-rw-r--r--fedora/.gitignore3
-rw-r--r--fedora/Makefile52
-rw-r--r--fedora/README.md39
-rw-r--r--fedora/jellyfin-firewalld.xml9
-rw-r--r--fedora/jellyfin-selinux-launcher.sh3
-rw-r--r--fedora/jellyfin-server-lowports.conf4
-rw-r--r--fedora/jellyfin.env44
-rw-r--r--fedora/jellyfin.override.conf53
-rw-r--r--fedora/jellyfin.service17
-rw-r--r--fedora/jellyfin.spec197
-rw-r--r--jellyfin.ruleset6
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs13
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj1
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs25
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs14
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs1
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs4
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs2
-rw-r--r--src/Jellyfin.Networking/AutoDiscoveryHost.cs42
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs12
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs28
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs71
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs18
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs107
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs8
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs6
229 files changed, 2360 insertions, 4868 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
deleted file mode 100644
index 547a514f8..000000000
--- a/.ci/azure-pipelines-abi.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-parameters:
-- name: Packages
- type: object
- default: {}
-- name: LinuxImage
- type: string
- default: "ubuntu-latest"
-- name: DotNetSdkVersion
- type: string
- default: 8.0.x
-
-jobs:
- - job: CompatibilityCheck
- displayName: Compatibility Check
- dependsOn: Build
- condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
-
- pool:
- vmImage: "${{ parameters.LinuxImage }}"
-
- strategy:
- matrix:
- ${{ each Package in parameters.Packages }}:
- ${{ Package.key }}:
- NugetPackageName: ${{ Package.value.NugetPackageName }}
- AssemblyFileName: ${{ Package.value.AssemblyFileName }}
- maxParallel: 2
-
- steps:
- - checkout: none
-
- - task: UseDotNet@2
- displayName: "Update DotNet"
- inputs:
- packageType: sdk
- version: ${{ parameters.DotNetSdkVersion }}
-
- - task: DotNetCoreCLI@2
- displayName: 'Install ABI CompatibilityChecker Tool'
- inputs:
- command: custom
- custom: tool
- arguments: 'update compatibilitychecker -g'
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download New Assembly Build Artifact'
- inputs:
- source: 'current'
- artifact: "$(NugetPackageName)"
- path: "$(System.ArtifactsDirectory)/new-artifacts"
- runVersion: "latest"
-
- - task: CopyFiles@2
- displayName: 'Copy New Assembly Build Artifact'
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/new-release
- cleanTargetFolder: true
- overWrite: true
- flattenFolders: true
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download Reference Assembly Build Artifact'
- enabled: false
- inputs:
- source: "specific"
- artifact: "$(NugetPackageName)"
- path: "$(System.ArtifactsDirectory)/current-artifacts"
- project: "$(System.TeamProjectId)"
- pipeline: "$(System.DefinitionId)"
- runVersion: "latestFromBranch"
- runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
-
- - task: CopyFiles@2
- displayName: 'Copy Reference Assembly Build Artifact'
- enabled: false
- inputs:
- sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
- contents: '**/*.dll'
- targetFolder: $(System.ArtifactsDirectory)/current-release
- cleanTargetFolder: true
- overWrite: true
- flattenFolders: true
-
- - task: DotNetCoreCLI@2
- displayName: 'Execute ABI Compatibility Check Tool'
- enabled: false
- inputs:
- command: custom
- custom: compat
- arguments: 'current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
- workingDirectory: $(System.ArtifactsDirectory)
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
deleted file mode 100644
index 0702aeb6b..000000000
--- a/.ci/azure-pipelines-main.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-parameters:
- LinuxImage: 'ubuntu-latest'
- RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 8.0.x
-
-jobs:
- - job: Build
- displayName: Build
- strategy:
- matrix:
- Release:
- BuildConfiguration: Release
- Debug:
- BuildConfiguration: Debug
- pool:
- vmImage: '${{ parameters.LinuxImage }}'
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: true
-
- - task: UseDotNet@2
- displayName: 'Update DotNet'
- inputs:
- packageType: sdk
- version: ${{ parameters.DotNetSdkVersion }}
-
- - task: DotNetCoreCLI@2
- displayName: 'Publish Server'
- inputs:
- command: publish
- publishWebProjects: false
- projects: '${{ parameters.RestoreBuildProjects }}'
- arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- zipAfterPublish: false
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Naming'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll'
- artifactName: 'Jellyfin.Naming'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Controller'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
- artifactName: 'Jellyfin.Controller'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Model'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
- artifactName: 'Jellyfin.Model'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Common'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
- artifactName: 'Jellyfin.Common'
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Artifact Extensions'
- condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
- inputs:
- targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
- artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
deleted file mode 100644
index b0684c0d4..000000000
--- a/.ci/azure-pipelines-package.yml
+++ /dev/null
@@ -1,274 +0,0 @@
-jobs:
-- job: BuildPackage
- displayName: 'Build Packages'
-
- strategy:
- matrix:
- CentOS.amd64:
- BuildConfiguration: centos.amd64
- Fedora.amd64:
- BuildConfiguration: fedora.amd64
- Debian.amd64:
- BuildConfiguration: debian.amd64
- Debian.arm64:
- BuildConfiguration: debian.arm64
- Debian.armhf:
- BuildConfiguration: debian.armhf
- Ubuntu.amd64:
- BuildConfiguration: ubuntu.amd64
- Ubuntu.arm64:
- BuildConfiguration: ubuntu.arm64
- Ubuntu.armhf:
- BuildConfiguration: ubuntu.armhf
- Linux.amd64:
- BuildConfiguration: linux.amd64
- Linux.amd64-musl:
- BuildConfiguration: linux.amd64-musl
- Linux.arm64:
- BuildConfiguration: linux.arm64
- Linux.musl-linux-arm64:
- BuildConfiguration: linux.musl-linux-arm64
- Linux.armhf:
- BuildConfiguration: linux.armhf
- Windows.amd64:
- BuildConfiguration: windows.amd64
- MacOS.amd64:
- BuildConfiguration: macos.amd64
- MacOS.arm64:
- BuildConfiguration: macos.arm64
- Portable:
- BuildConfiguration: portable
-
- pool:
- vmImage: 'ubuntu-latest'
-
- steps:
- - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
- displayName: Set release version (stable)
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
- displayName: 'Build Dockerfile'
-
- - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
- displayName: 'Run Dockerfile (unstable)'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
-
- - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
- displayName: 'Run Dockerfile (stable)'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish Release'
- inputs:
- targetPath: '$(Build.SourcesDirectory)/deployment/dist'
- artifactName: 'jellyfin-server-$(BuildConfiguration)'
-
- - task: SSH@0
- displayName: 'Create target directory on repository server'
- inputs:
- sshEndpoint: repository
- runOptions: 'inline'
- inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Upload artifacts to repository server'
- inputs:
- sshEndpoint: repository
- sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
- contents: '**'
- targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
-
-- job: OpenAPISpec
- dependsOn: Test
- condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
- displayName: 'Push OpenAPI Spec to repository'
-
- pool:
- vmImage: 'ubuntu-latest'
-
- steps:
- - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
- displayName: Set release version (stable)
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download OpenAPI Spec'
- inputs:
- source: 'current'
- artifact: "OpenAPI Spec"
- path: "$(System.ArtifactsDirectory)/openapispec"
- runVersion: "latest"
-
- - task: SSH@0
- displayName: 'Create target directory on repository server'
- inputs:
- sshEndpoint: repository
- runOptions: 'inline'
- inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
-
- - task: CopyFilesOverSSH@0
- displayName: 'Upload artifacts to repository server'
- inputs:
- sshEndpoint: repository
- sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
- contents: 'openapi.json'
- targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
-
-- job: BuildDocker
- displayName: 'Build Docker'
-
- strategy:
- matrix:
- amd64:
- BuildConfiguration: amd64
- arm64:
- BuildConfiguration: arm64
- armhf:
- BuildConfiguration: armhf
-
- pool:
- vmImage: 'ubuntu-latest'
-
- variables:
- - name: JellyfinVersion
- value: 0.0.0
-
- steps:
- - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
- displayName: Set release version (stable)
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
-
- - task: Docker@2
- displayName: 'Push Unstable Image'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- repository: 'jellyfin/jellyfin-server'
- command: buildAndPush
- buildContext: '.'
- Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
- containerRegistry: Docker Hub
- tags: |
- unstable-$(Build.BuildNumber)-$(BuildConfiguration)
- unstable-$(BuildConfiguration)
-
- - task: Docker@2
- displayName: 'Push Stable Image'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- repository: 'jellyfin/jellyfin-server'
- command: buildAndPush
- buildContext: '.'
- Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
- containerRegistry: Docker Hub
- tags: |
- stable-$(Build.BuildNumber)-$(BuildConfiguration)
- $(JellyfinVersion)-$(BuildConfiguration)
-
-- job: CollectArtifacts
- timeoutInMinutes: 20
- displayName: 'Collect Artifacts'
- condition: succeededOrFailed()
- continueOnError: true
- dependsOn:
- - BuildPackage
- - BuildDocker
-
- pool:
- vmImage: 'ubuntu-latest'
-
- steps:
- - task: SSH@0
- displayName: 'Update Unstable Repository'
- continueOnError: true
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- sshEndpoint: repository
- runOptions: 'commands'
- commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
-
- - task: SSH@0
- displayName: 'Update Stable Repository'
- continueOnError: true
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- sshEndpoint: repository
- runOptions: 'commands'
- commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
-
-- job: PublishNuget
- displayName: 'Publish NuGet packages'
-
- pool:
- vmImage: 'ubuntu-latest'
-
- variables:
- - name: JellyfinVersion
- value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
-
- steps:
- - task: UseDotNet@2
- displayName: 'Use .NET 8.0 sdk'
- inputs:
- packageType: 'sdk'
- version: '8.0.x'
-
- - task: DotNetCoreCLI@2
- displayName: 'Build Stable Nuget packages'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- command: 'custom'
- projects: |
- Jellyfin.Data/Jellyfin.Data.csproj
- MediaBrowser.Common/MediaBrowser.Common.csproj
- MediaBrowser.Controller/MediaBrowser.Controller.csproj
- MediaBrowser.Model/MediaBrowser.Model.csproj
- Emby.Naming/Emby.Naming.csproj
- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
- custom: 'pack'
- arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
-
- - task: DotNetCoreCLI@2
- displayName: 'Build Unstable Nuget packages'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- command: 'custom'
- projects: |
- Jellyfin.Data/Jellyfin.Data.csproj
- MediaBrowser.Common/MediaBrowser.Common.csproj
- MediaBrowser.Controller/MediaBrowser.Controller.csproj
- MediaBrowser.Model/MediaBrowser.Model.csproj
- Emby.Naming/Emby.Naming.csproj
- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
- custom: 'pack'
- arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
-
- - task: PublishBuildArtifacts@1
- displayName: 'Publish Nuget packages'
- inputs:
- pathToPublish: $(Build.ArtifactStagingDirectory)
- artifactName: Jellyfin Nuget Packages
-
- - task: NuGetCommand@2
- displayName: 'Push Nuget packages to stable feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- inputs:
- command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
- nuGetFeedType: 'external'
- publishFeedCredentials: 'NugetOrg'
- allowPackageConflicts: true # This ignores an error if the version already exists
-
- - task: NuGetAuthenticate@1
- displayName: 'Authenticate to unstable Nuget feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
-
- - task: NuGetCommand@2
- displayName: 'Push Nuget packages to unstable feed'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- inputs:
- command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it
- nuGetFeedType: 'internal'
- publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327'
- allowPackageConflicts: true # This ignores an error if the version already exists
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
deleted file mode 100644
index 3549c691c..000000000
--- a/.ci/azure-pipelines-test.yml
+++ /dev/null
@@ -1,98 +0,0 @@
-parameters:
-- name: ImageNames
- type: object
- default:
- Linux: "ubuntu-latest"
- Windows: "windows-latest"
- macOS: "macos-latest"
-- name: TestProjects
- type: string
- default: "tests/**/*Tests.csproj"
-- name: DotNetSdkVersion
- type: string
- default: 8.0.x
-
-jobs:
- - job: Test
- displayName: Test
- strategy:
- matrix:
- ${{ each imageName in parameters.ImageNames }}:
- ${{ imageName.key }}:
- ImageName: ${{ imageName.value }}
- pool:
- vmImage: "$(ImageName)"
- steps:
- - checkout: self
- clean: true
- submodules: true
- persistCredentials: false
-
- # This is required for the SonarCloud analyzer
- - task: UseDotNet@2
- displayName: "Install .NET SDK 5.x"
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- inputs:
- packageType: sdk
- version: '5.x'
-
- - task: UseDotNet@2
- displayName: "Update DotNet"
- inputs:
- packageType: sdk
- version: ${{ parameters.DotNetSdkVersion }}
-
- - task: SonarCloudPrepare@1
- displayName: 'Prepare analysis on SonarCloud'
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- enabled: false
- inputs:
- SonarCloud: 'Sonarcloud for Jellyfin'
- organization: 'jellyfin'
- projectKey: 'jellyfin_jellyfin'
-
- - task: DotNetCoreCLI@2
- displayName: 'Run CLI Tests'
- inputs:
- command: "test"
- projects: ${{ parameters.TestProjects }}
- arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
- publishTestResults: true
- testRunTitle: $(Agent.JobName)
- workingDirectory: "$(Build.SourcesDirectory)"
-
- - task: SonarCloudAnalyze@1
- displayName: 'Run Code Analysis'
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- enabled: false
-
- - task: SonarCloudPublish@1
- displayName: 'Publish Quality Gate Result'
- condition: eq(variables['ImageName'], 'ubuntu-latest')
- enabled: false
-
- - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
- condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
- displayName: 'Run ReportGenerator'
- inputs:
- reports: "$(Agent.TempDirectory)/**/coverage.cobertura.xml"
- targetdir: "$(Agent.TempDirectory)/merged/"
- reporttypes: "Cobertura"
-
- ## V2 is already in the repository but it does not work "wrong number of segments" YAML error.
- - task: PublishCodeCoverageResults@1
- condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) # !! THIS is for V1 only V2 will/should support merging
- displayName: 'Publish Code Coverage'
- inputs:
- codeCoverageTool: "cobertura"
- #summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml' # !!THIS IS FOR V2
- summaryFileLocation: "$(Agent.TempDirectory)/merged/**.xml"
- pathToSources: $(Build.SourcesDirectory)
- failIfCoverageEmpty: true
-
- - task: PublishPipelineArtifact@1
- displayName: 'Publish OpenAPI Artifact'
- condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- inputs:
- targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json"
- artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
deleted file mode 100644
index 19c9caacb..000000000
--- a/.ci/azure-pipelines.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: $(Date:yyyyMMdd)$(Rev:.r)
-
-variables:
-- name: TestProjects
- value: 'tests/**/*Tests.csproj'
-- name: RestoreBuildProjects
- value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-
-pr:
- autoCancel: true
-
-trigger:
- batch: true
- branches:
- include:
- - '*'
- tags:
- include:
- - 'v*'
-
-jobs:
-- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}:
- - template: azure-pipelines-main.yml
- parameters:
- LinuxImage: 'ubuntu-latest'
- RestoreBuildProjects: $(RestoreBuildProjects)
-
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- - template: azure-pipelines-test.yml
- parameters:
- ImageNames:
- Linux: 'ubuntu-latest'
- Windows: 'windows-latest'
- macOS: 'macos-latest'
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- - template: azure-pipelines-test.yml
- parameters:
- ImageNames:
- Linux: 'ubuntu-latest'
-
-- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- - template: azure-pipelines-abi.yml
- parameters:
- Packages:
- Naming:
- NugetPackageName: Jellyfin.Naming
- AssemblyFileName: Emby.Naming.dll
- Controller:
- NugetPackageName: Jellyfin.Controller
- AssemblyFileName: MediaBrowser.Controller.dll
- Model:
- NugetPackageName: Jellyfin.Model
- AssemblyFileName: MediaBrowser.Model.dll
- Common:
- NugetPackageName: Jellyfin.Common
- AssemblyFileName: MediaBrowser.Common.dll
- Extensions:
- NugetPackageName: Jellyfin.Extensions
- AssemblyFileName: Jellyfin.Extensions.dll
- LinuxImage: 'ubuntu-latest'
-
-- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- - template: azure-pipelines-package.yml
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index d9b689bb6..8e82e3001 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.2",
+ "version": "8.0.4",
"commands": [
"dotnet-ef"
]
diff --git a/.copr b/.copr
deleted file mode 120000
index 100fe0cd7..000000000
--- a/.copr
+++ /dev/null
@@ -1 +0,0 @@
-fedora \ No newline at end of file
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 20307dd7d..39fe6f1d2 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
+ uses: github/codeql-action/init@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
+ uses: github/codeql-action/autobuild@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
+ uses: github/codeql-action/analyze@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index b5ccafb86..bdbfcd3eb 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -105,7 +105,7 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.0
+ uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -137,3 +137,60 @@ jobs:
<!--openapi-diff-workflow-comment-->
No changes to OpenAPI specification found. See history of this comment for previous changes.
+
+ publish:
+ name: OpenAPI - Publish Unstable Spec
+ if: |
+ github.event_name != 'pull_request_target' &&
+ contains(github.repository_owner, 'jellyfin')
+ runs-on: ubuntu-latest
+ needs:
+ - openapi-head
+ steps:
+ - name: Set unstable dated version
+ id: version
+ run: |-
+ echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
+ - name: Download openapi-head
+ uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
+ with:
+ name: openapi-head
+ path: openapi-head
+ - name: Upload openapi.json (unstable) to repository server
+ uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
+ with:
+ host: "${{ secrets.REPO_HOST }}"
+ username: "${{ secrets.REPO_USER }}"
+ key: "${{ secrets.REPO_KEY }}"
+ source: openapi-head/openapi.json
+ strip_components: 1
+ target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
+ - name: Move openapi.json (unstable) into place
+ uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
+ with:
+ host: "${{ secrets.REPO_HOST }}"
+ username: "${{ secrets.REPO_USER }}"
+ key: "${{ secrets.REPO_KEY }}"
+ debug: false
+ script_stop: false
+ script: |
+ TGT_DIR="/srv/repository/main/openapi"
+ LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
+ # If new and previous spec don't differ (diff retcode 0), remove incoming and finish
+ if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
+ rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
+ exit 0
+ fi
+ # Move new spec into place
+ sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
+ # Delete previous jellyfin-openapi-unstable_previous.json
+ sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
+ # Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
+ sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
+ # Create new jellyfin-openapi-stable.json symlink
+ sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
+ # Check that the previous openapi spec is correct
+ if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
+ sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
+ sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
+ fi
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 8ee6b3028..612e8c751 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@b067e0c5d288fb4277b9f397b2dc6013f60381f0 # 5.2.2
+ uses: danielpalme/ReportGenerator-GitHub-Action@3e39bd1b454c2bac14560547e4394f9317672705 # 5.2.4
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 386f8d321..5055bbfa5 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -121,3 +121,28 @@ jobs:
${{ steps.run_tests.outputs.output }}
reactions: confused
+
+ rename:
+ name: Rename
+ if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
+ runs-on: ubuntu-latest
+ steps:
+ - name: pull in script
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ repository: jellyfin/jellyfin-triage-script
+ - name: install python
+ uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
+ with:
+ python-version: '3.12'
+ cache: 'pip'
+ - name: install python packages
+ run: pip install -r rename/requirements.txt
+ - name: run rename script
+ run: python3 rename.py
+ working-directory: ./rename
+ env:
+ GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ ISSUE: ${{ github.event.issue.number }}
+ COMMENT_ID: ${{ github.event.comment.id }}
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
new file mode 100644
index 000000000..e53234641
--- /dev/null
+++ b/.github/workflows/issue-template-check.yml
@@ -0,0 +1,29 @@
+name: Check Issue Template
+on:
+ issues:
+ types:
+ - opened
+jobs:
+ check_issue:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - name: pull in script
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ repository: jellyfin/jellyfin-triage-script
+ - name: install python
+ uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
+ with:
+ python-version: '3.12'
+ cache: 'pip'
+ - name: install python packages
+ run: pip install -r main-repo-triage/requirements.txt
+ - name: check and comment issue
+ working-directory: ./main-repo-triage
+ run: python3 single_issue_gha.py
+ env:
+ GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ ISSUE: ${{ github.event.issue.number }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 55642e4e2..8550222a1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -177,9 +177,12 @@
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
- [Pithaya](https://github.com/Pithaya)
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
- _ [Barasingha](https://github.com/MaVdbussche)
+ - [Barasingha](https://github.com/MaVdbussche)
- [Gauvino](https://github.com/Gauvino)
- [felix920506](https://github.com/felix920506)
+ - [btopherjohnson](https://github.com/btopherjohnson)
+ - [GeorgeH005](https://github.com/GeorgeH005)
+ - [Vedant](https://github.com/viktory36/)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7400edd93..10635cd64 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,44 +8,45 @@
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
- <PackageVersion Include="BDInfo" Version="0.7.6.2" />
+ <PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.2" />
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
- <PackageVersion Include="coverlet.collector" Version="6.0.1" />
+ <PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.27" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.3" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.3.1" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
+ <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="3.6.13" />
+ <PackageVersion Include="libse" Version="4.0.5" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.2" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.4" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
@@ -67,25 +68,25 @@
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
- <PackageVersion Include="SkiaSharp" Version="2.88.7" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.7" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
+ <PackageVersion Include="SkiaSharp" Version="2.88.8" />
+ <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="1.0.0.14" />
+ <PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
- <PackageVersion Include="System.Text.Json" Version="8.0.2" />
+ <PackageVersion Include="System.Text.Json" Version="8.0.3" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="TMDbLib" Version="2.1.0" />
+ <PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
- <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
+ <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageVersion Include="xunit" Version="2.7.0" />
+ <PackageVersion Include="xunit" Version="2.7.1" />
</ItemGroup>
-</Project>
+</Project> \ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 550c3203d..000000000
--- a/Dockerfile
+++ /dev/null
@@ -1,87 +0,0 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
-#####################################
-# Requires binfm_misc registration
-# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=8.0
-
-FROM node:20-alpine as web-builder
-ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
- && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
- && apk del curl \
- && cd jellyfin-web-* \
- && npm ci --no-audit --unsafe-perm \
- && npm run build:production \
- && mv dist /dist
-
-FROM debian:bookworm-slim as app
-
-# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
-ARG DEBIAN_FRONTEND="noninteractive"
-# http://stackoverflow.com/questions/48162574/ddg#49462622
-ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
-# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
-ENV NVIDIA_VISIBLE_DEVICES="all"
-ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
-
-ENV JELLYFIN_DATA_DIR=/config
-ENV JELLYFIN_CACHE_DIR=/cache
-
-# https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=22.3.11.ci17757293
-ARG IGC_VERSION=1.0.15136.22
-ARG NEO_VERSION=23.39.27427.23
-ARG LEVEL_ZERO_VERSION=1.3.27427.23
-
-RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
- && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
- && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
- && apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y mesa-va-drivers jellyfin-ffmpeg6 openssl locales \
-# Intel VAAPI Tone mapping dependencies:
-# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
-# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
- && mkdir intel-compute-runtime \
- && cd intel-compute-runtime \
- && curl -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
- -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
- -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
- -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
- -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/libigdgmm12_${GMMLIB_VERSION}_amd64.deb \
- && dpkg -i *.deb \
- && cd .. \
- && rm -rf intel-compute-runtime \
- && apt-get remove gnupg -y \
- && apt-get clean autoclean -y \
- && apt-get autoremove -y \
- && rm -rf /var/lib/apt/lists/* \
- && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
- && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
- && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-
-ENV LC_ALL=en_US.UTF-8
-ENV LANG=en_US.UTF-8
-ENV LANGUAGE=en_US:en
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
-
-FROM app
-
-ENV HEALTHCHECK_URL=http://localhost:8096/health
-
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-EXPOSE 8096
-VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
-ENTRYPOINT [ "./jellyfin/jellyfin", \
- "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
-
-HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
- CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
deleted file mode 100644
index 07039e43b..000000000
--- a/Dockerfile.arm
+++ /dev/null
@@ -1,74 +0,0 @@
-# DESIGNED FOR BUILDING ON ARM ONLY
-#####################################
-# Requires binfm_misc registration
-# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=8.0
-
-FROM node:20-alpine as web-builder
-ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
- && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
- && apk del curl \
- && cd jellyfin-web-* \
- && npm ci --no-audit --unsafe-perm \
- && npm run build:production \
- && mv dist /dist
-
-FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:bookworm-slim as app
-
-# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
-ARG DEBIAN_FRONTEND="noninteractive"
-# http://stackoverflow.com/questions/48162574/ddg#49462622
-ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
-# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
-ENV NVIDIA_VISIBLE_DEVICES="all"
-ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
-
-ENV JELLYFIN_DATA_DIR=/config
-ENV JELLYFIN_CACHE_DIR=/cache
-
-COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
-
-RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
- && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
- && curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
- && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
- && apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y \
- jellyfin-ffmpeg6 libssl-dev libfontconfig1 \
- libfreetype6 vainfo libva2 locales \
- && apt-get remove gnupg -y \
- && apt-get clean autoclean -y \
- && apt-get autoremove -y \
- && rm -rf /var/lib/apt/lists/* \
- && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
- && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
- && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-
-ENV LC_ALL=en_US.UTF-8
-ENV LANG=en_US.UTF-8
-ENV LANGUAGE=en_US:en
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
-
-FROM app
-
-ENV HEALTHCHECK_URL=http://localhost:8096/health
-
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-EXPOSE 8096
-VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
-ENTRYPOINT [ "/jellyfin/jellyfin", \
- "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
-
-HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
- CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
deleted file mode 100644
index 54023794f..000000000
--- a/Dockerfile.arm64
+++ /dev/null
@@ -1,74 +0,0 @@
-# DESIGNED FOR BUILDING ON ARM64 ONLY
-#####################################
-# Requires binfm_misc registration
-# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=8.0
-
-FROM node:20-alpine as web-builder
-ARG JELLYFIN_WEB_VERSION=master
-RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
- && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
- && apk del curl \
- && cd jellyfin-web-* \
- && npm ci --no-audit --unsafe-perm \
- && npm run build:production \
- && mv dist /dist
-
-FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:bookworm-slim as app
-
-# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
-ARG DEBIAN_FRONTEND="noninteractive"
-# http://stackoverflow.com/questions/48162574/ddg#49462622
-ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
-# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
-ENV NVIDIA_VISIBLE_DEVICES="all"
-ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
-
-ENV JELLYFIN_DATA_DIR=/config
-ENV JELLYFIN_CACHE_DIR=/cache
-
-COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
-
-RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
- && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
- && curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
- && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
- && apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y \
- jellyfin-ffmpeg6 locales libssl-dev libfontconfig1 \
- libfreetype6 libomxil-bellagio0 libomxil-bellagio-bin \
- && apt-get remove gnupg -y \
- && apt-get clean autoclean -y \
- && apt-get autoremove -y \
- && rm -rf /var/lib/apt/lists/* \
- && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
- && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
- && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-
-ENV LC_ALL=en_US.UTF-8
-ENV LANG=en_US.UTF-8
-ENV LANGUAGE=en_US:en
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
-WORKDIR /repo
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
-
-FROM app
-
-ENV HEALTHCHECK_URL=http://localhost:8096/health
-
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-EXPOSE 8096
-VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
-ENTRYPOINT [ "/jellyfin/jellyfin", \
- "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
-
-HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
- CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 4bd226d95..333d237a2 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -540,6 +540,12 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
+ "extra",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Unknown,
+ ExtraRuleType.DirectoryName,
"other",
MediaType.Video),
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index 27329a7f2..e2f1ca813 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -16,167 +16,160 @@ using TagLib.IFD;
using TagLib.IFD.Entries;
using TagLib.IFD.Tags;
-namespace Emby.Photos
+namespace Emby.Photos;
+
+/// <summary>
+/// Metadata provider for photos.
+/// </summary>
+public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
{
+ private readonly ILogger<PhotoProvider> _logger;
+ private readonly IImageProcessor _imageProcessor;
+
+ // These are causing taglib to hang
+ private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
+
/// <summary>
- /// Metadata provider for photos.
+ /// Initializes a new instance of the <see cref="PhotoProvider" /> class.
/// </summary>
- public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
+ /// <param name="logger">The logger.</param>
+ /// <param name="imageProcessor">The image processor.</param>
+ public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
{
- private readonly ILogger<PhotoProvider> _logger;
- private readonly IImageProcessor _imageProcessor;
-
- // These are causing taglib to hang
- private readonly string[] _includeExtensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif" };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PhotoProvider" /> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="imageProcessor">The image processor.</param>
- public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
- {
- _logger = logger;
- _imageProcessor = imageProcessor;
- }
+ _logger = logger;
+ _imageProcessor = imageProcessor;
+ }
- /// <inheritdoc />
- public string Name => "Embedded Information";
+ /// <inheritdoc />
+ public string Name => "Embedded Information";
- /// <inheritdoc />
- public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ if (item.IsFileProtocol)
{
- if (item.IsFileProtocol)
- {
- var file = directoryService.GetFile(item.Path);
- return file is not null && file.LastWriteTimeUtc != item.DateModified;
- }
-
- return false;
+ var file = directoryService.GetFile(item.Path);
+ return file is not null && file.LastWriteTimeUtc != item.DateModified;
}
- /// <inheritdoc />
- public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- item.SetImagePath(ImageType.Primary, item.Path);
+ return false;
+ }
- // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
+ /// <inheritdoc />
+ public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ item.SetImagePath(ImageType.Primary, item.Path);
+
+ // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
+ {
+ try
{
- try
+ using var file = TagLib.File.Create(item.Path);
+ if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
{
- using (var file = TagLib.File.Create(item.Path))
+ var structure = tag.Structure;
+ if (structure?.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{
- if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
+ var exifStructure = exif.Structure;
+ if (exifStructure is not null)
{
- var structure = tag.Structure;
- if (structure is not null
- && structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
+ if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) is RationalIFDEntry apertureEntry)
{
- var exifStructure = exif.Structure;
- if (exifStructure is not null)
- {
- var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
- if (entry is not null)
- {
- item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
- }
-
- entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
- if (entry is not null)
- {
- item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
- }
- }
+ item.Aperture = (double)apertureEntry.Value.Numerator / apertureEntry.Value.Denominator;
+ }
+
+ if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) is RationalIFDEntry shutterSpeedEntry)
+ {
+ item.ShutterSpeed = (double)shutterSpeedEntry.Value.Numerator / shutterSpeedEntry.Value.Denominator;
}
}
+ }
+ }
- if (file is TagLib.Image.File image)
- {
- item.CameraMake = image.ImageTag.Make;
- item.CameraModel = image.ImageTag.Model;
+ if (file is TagLib.Image.File image)
+ {
+ item.CameraMake = image.ImageTag.Make;
+ item.CameraModel = image.ImageTag.Model;
- item.Width = image.Properties.PhotoWidth;
- item.Height = image.Properties.PhotoHeight;
+ item.Width = image.Properties.PhotoWidth;
+ item.Height = image.Properties.PhotoHeight;
- var rating = image.ImageTag.Rating;
- item.CommunityRating = rating.HasValue ? rating : null;
+ item.CommunityRating = image.ImageTag.Rating;
- item.Overview = image.ImageTag.Comment;
+ item.Overview = image.ImageTag.Comment;
- if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
- && !item.LockedFields.Contains(MetadataField.Name))
- {
- item.Name = image.ImageTag.Title;
- }
+ if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
+ && !item.LockedFields.Contains(MetadataField.Name))
+ {
+ item.Name = image.ImageTag.Title;
+ }
- var dateTaken = image.ImageTag.DateTime;
- if (dateTaken.HasValue)
- {
- item.DateCreated = dateTaken.Value;
- item.PremiereDate = dateTaken.Value;
- item.ProductionYear = dateTaken.Value.Year;
- }
+ var dateTaken = image.ImageTag.DateTime;
+ if (dateTaken.HasValue)
+ {
+ item.DateCreated = dateTaken.Value;
+ item.PremiereDate = dateTaken.Value;
+ item.ProductionYear = dateTaken.Value.Year;
+ }
- item.Genres = image.ImageTag.Genres;
- item.Tags = image.ImageTag.Keywords;
- item.Software = image.ImageTag.Software;
+ item.Genres = image.ImageTag.Genres;
+ item.Tags = image.ImageTag.Keywords;
+ item.Software = image.ImageTag.Software;
- if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
- {
- item.Orientation = null;
- }
- else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
- {
- item.Orientation = orientation;
- }
+ if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
+ {
+ item.Orientation = null;
+ }
+ else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
+ {
+ item.Orientation = orientation;
+ }
- item.ExposureTime = image.ImageTag.ExposureTime;
- item.FocalLength = image.ImageTag.FocalLength;
+ item.ExposureTime = image.ImageTag.ExposureTime;
+ item.FocalLength = image.ImageTag.FocalLength;
- item.Latitude = image.ImageTag.Latitude;
- item.Longitude = image.ImageTag.Longitude;
- item.Altitude = image.ImageTag.Altitude;
+ item.Latitude = image.ImageTag.Latitude;
+ item.Longitude = image.ImageTag.Longitude;
+ item.Altitude = image.ImageTag.Altitude;
- if (image.ImageTag.ISOSpeedRatings.HasValue)
- {
- item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
- }
- else
- {
- item.IsoSpeedRating = null;
- }
- }
+ if (image.ImageTag.ISOSpeedRatings.HasValue)
+ {
+ item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
+ }
+ else
+ {
+ item.IsoSpeedRating = null;
}
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
}
}
-
- if (item.Width <= 0 || item.Height <= 0)
+ catch (Exception ex)
{
- var img = item.GetImageInfo(ImageType.Primary, 0);
+ _logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
+ }
+ }
- try
- {
- var size = _imageProcessor.GetImageDimensions(item, img);
+ if (item.Width <= 0 || item.Height <= 0)
+ {
+ var img = item.GetImageInfo(ImageType.Primary, 0);
- if (size.Width > 0 && size.Height > 0)
- {
- item.Width = size.Width;
- item.Height = size.Height;
- }
- }
- catch (ArgumentException)
+ try
+ {
+ var size = _imageProcessor.GetImageDimensions(item, img);
+
+ if (size.Width > 0 && size.Height > 0)
{
- // format not supported
+ item.Width = size.Width;
+ item.Height = size.Height;
}
}
-
- const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
- return Task.FromResult(Result);
+ catch (ArgumentException)
+ {
+ // format not supported
+ }
}
+
+ const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
+ return Task.FromResult(Result);
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 745753440..6add7e0b3 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -109,13 +109,13 @@ namespace Emby.Server.Implementations
/// <summary>
/// The disposable parts.
/// </summary>
- private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
+ private readonly ConcurrentBag<IDisposable> _disposableParts = new();
private readonly DeviceId _deviceId;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
- private readonly IPluginManager _pluginManager;
+ private readonly PluginManager _pluginManager;
private List<Type> _creatingInstances;
@@ -146,7 +146,7 @@ namespace Emby.Server.Implementations
_startupConfig = startupConfig;
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
- _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
+ _deviceId = new DeviceId(ApplicationPaths, LoggerFactory.CreateLogger<DeviceId>());
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -161,7 +161,7 @@ namespace Emby.Server.Implementations
ApplicationPaths.PluginsPath,
ApplicationVersion);
- _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
+ _disposableParts.Add(_pluginManager);
}
/// <summary>
@@ -360,7 +360,7 @@ namespace Emby.Server.Implementations
{
foreach (var part in parts.OfType<IDisposable>())
{
- _disposableParts.TryAdd(part, byte.MinValue);
+ _disposableParts.Add(part);
}
}
@@ -381,7 +381,7 @@ namespace Emby.Server.Implementations
{
foreach (var part in parts.OfType<IDisposable>())
{
- _disposableParts.TryAdd(part, byte.MinValue);
+ _disposableParts.Add(part);
}
}
@@ -457,7 +457,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IApplicationHost>(this);
- serviceCollection.AddSingleton(_pluginManager);
+ serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
@@ -965,7 +965,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name);
- foreach (var (part, _) in _disposableParts)
+ foreach (var part in _disposableParts.ToArray())
{
var partType = part.GetType();
if (partType == type)
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index b34d0f21e..e414792ba 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections");
- await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
+ await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First();
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index bf079d90c..b1c99227c 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -186,10 +186,7 @@ namespace Emby.Server.Implementations.Data
protected void CheckDisposed()
{
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
- }
+ ObjectDisposedException.ThrowIf(_disposed, this);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index a6336f145..59e4ff1a9 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -205,7 +205,7 @@ namespace Emby.Server.Implementations.Data
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
- private static readonly string _mediaAttachmentInsertPrefix;
+ private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix();
private static readonly BaseItemKind[] _programTypes = new[]
{
@@ -296,21 +296,6 @@ namespace Emby.Server.Implementations.Data
{ BaseItemKind.Year, typeof(Year).FullName }
};
- static SqliteItemRepository()
- {
- var queryPrefixText = new StringBuilder();
- queryPrefixText.Append("insert into mediaattachments (");
- foreach (var column in _mediaAttachmentSaveColumns)
- {
- queryPrefixText.Append(column)
- .Append(',');
- }
-
- queryPrefixText.Length -= 1;
- queryPrefixText.Append(") values ");
- _mediaAttachmentInsertPrefix = queryPrefixText.ToString();
- }
-
/// <summary>
/// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
/// </summary>
@@ -5879,6 +5864,21 @@ AND Type = @InternalPersonType)");
return item;
}
+ private static string BuildMediaAttachmentInsertPrefix()
+ {
+ var queryPrefixText = new StringBuilder();
+ queryPrefixText.Append("insert into mediaattachments (");
+ foreach (var column in _mediaAttachmentSaveColumns)
+ {
+ queryPrefixText.Append(column)
+ .Append(',');
+ }
+
+ queryPrefixText.Length -= 1;
+ queryPrefixText.Append(") values ");
+ return queryPrefixText.ToString();
+ }
+
#nullable enable
private readonly struct QueryTimeLogger : IDisposable
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index a5edcc58c..20359e4ad 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -58,7 +58,8 @@ namespace Emby.Server.Implementations.Data
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
+ "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
+ "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
if (!userDataTableExists)
{
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index b3f5549bc..2459178d8 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -17,19 +15,19 @@ namespace Emby.Server.Implementations.Devices
private readonly ILogger<DeviceId> _logger;
private readonly object _syncLock = new object();
- private string _id;
+ private string? _id;
- public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
+ public DeviceId(IApplicationPaths appPaths, ILogger<DeviceId> logger)
{
_appPaths = appPaths;
- _logger = loggerFactory.CreateLogger<DeviceId>();
+ _logger = logger;
}
- public string Value => _id ?? (_id = GetDeviceId());
+ public string Value => _id ??= GetDeviceId();
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
- private string GetCachedId()
+ private string? GetCachedId()
{
try
{
@@ -65,7 +63,7 @@ namespace Emby.Server.Implementations.Devices
{
var path = CachePath;
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
lock (_syncLock)
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 7812687ea..98eacb52b 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -668,12 +668,13 @@ namespace Emby.Server.Implementations.Dto
{
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
- if (!dto.ImageBlurHashes.ContainsKey(image.Type))
+ if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{
- dto.ImageBlurHashes[image.Type] = new Dictionary<string, string>();
+ value = new Dictionary<string, string>();
+ dto.ImageBlurHashes[image.Type] = value;
}
- dto.ImageBlurHashes[image.Type][tag] = image.BlurHash;
+ value[tag] = image.BlurHash;
}
return tag;
@@ -903,10 +904,7 @@ namespace Emby.Server.Implementations.Dto
if (item is Audio audio)
{
dto.Album = audio.Album;
- if (audio.ExtraType.HasValue)
- {
- dto.ExtraType = audio.ExtraType.Value.ToString();
- }
+ dto.ExtraType = audio.ExtraType;
var albumParent = audio.AlbumEntity;
@@ -1058,10 +1056,7 @@ namespace Emby.Server.Implementations.Dto
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
}
- if (video.ExtraType.HasValue)
- {
- dto.ExtraType = video.ExtraType.Value.ToString();
- }
+ dto.ExtraType = video.ExtraType;
}
if (options.ContainsField(ItemFields.MediaStreams))
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 52f14b0b1..774d3563c 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
- using var connection = new WebSocketConnection(
+ var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
authorizationInfo,
@@ -56,17 +56,19 @@ namespace Emby.Server.Implementations.HttpServer
{
OnReceive = ProcessWebSocketMessageReceived
};
-
- var tasks = new Task[_webSocketListeners.Length];
- for (var i = 0; i < _webSocketListeners.Length; ++i)
+ await using (connection.ConfigureAwait(false))
{
- tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
- }
+ var tasks = new Task[_webSocketListeners.Length];
+ for (var i = 0; i < _webSocketListeners.Length; ++i)
+ {
+ tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
+ }
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ await Task.WhenAll(tasks).ConfigureAwait(false);
- await connection.ReceiveAsync().ConfigureAwait(false);
- _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+ await connection.ReceiveAsync().ConfigureAwait(false);
+ _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+ }
}
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
{
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index c380d67db..67854a2a7 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Security;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
@@ -643,7 +644,15 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
- return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
+ try
+ {
+ return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
+ }
+ catch (Exception ex) when (ex is UnauthorizedAccessException or DirectoryNotFoundException or SecurityException)
+ {
+ _logger.LogError(ex, "Failed to enumerate path {Path}", path);
+ return Enumerable.Empty<string>();
+ }
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a2abafd2a..bb5cc746e 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -338,7 +338,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
- "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -347,7 +347,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
- "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -366,7 +366,7 @@ namespace Emby.Server.Implementations.Library
}
_logger.LogDebug(
- "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
metadataPath,
@@ -395,7 +395,7 @@ namespace Emby.Server.Implementations.Library
try
{
_logger.LogInformation(
- "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
item.GetType().Name,
item.Name ?? "Unknown name",
fileSystemInfo.FullName,
@@ -410,6 +410,24 @@ namespace Emby.Server.Implementations.Library
File.Delete(fileSystemInfo.FullName);
}
}
+ catch (DirectoryNotFoundException)
+ {
+ _logger.LogInformation(
+ "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+ }
+ catch (FileNotFoundException)
+ {
+ _logger.LogInformation(
+ "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+ }
catch (IOException)
{
if (isRequiredForDelete)
@@ -443,7 +461,7 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
- private static IEnumerable<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
+ private static List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
{
var list = new List<string>
{
@@ -2677,7 +2695,12 @@ namespace Emby.Server.Implementations.Library
extra = itemById;
}
- extra.ExtraType = extraType;
+ // Only update extra type if it is more specific then the currently known extra type
+ if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
+ {
+ extra.ExtraType = extraType;
+ }
+
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
return extra;
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index d4aeae41a..0ebfe3ae7 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -37,16 +35,16 @@ namespace Emby.Server.Implementations.Library
_appPaths = appPaths;
}
- public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, CancellationToken cancellationToken)
+ public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string? cacheKey, bool addProbeDelay, CancellationToken cancellationToken)
{
var originalRuntime = mediaSource.RunTimeTicks;
var now = DateTime.UtcNow;
- MediaInfo mediaInfo = null;
+ MediaInfo? mediaInfo = null;
var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ".json");
- if (!string.IsNullOrEmpty(cacheKey))
+ if (cacheFilePath is not null)
{
try
{
@@ -91,7 +89,7 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
- Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath) ?? throw new InvalidOperationException("Path can't be a root directory."));
FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 18ada6aeb..9658bd566 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
if (user is not null)
{
- SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
+ SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
if (item.MediaType == MediaType.Audio)
{
@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Library
catch (Exception ex)
{
_logger.LogError(ex, "Error getting media sources");
- return Enumerable.Empty<MediaSourceInfo>();
+ return [];
}
}
@@ -339,7 +339,7 @@ namespace Emby.Server.Implementations.Library
{
foreach (var source in sources)
{
- SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
+ SetDefaultAudioAndSubtitleStreamIndices(item, source, user);
if (item.MediaType == MediaType.Audio)
{
@@ -360,7 +360,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.IsNullOrEmpty(language))
{
- return Array.Empty<string>();
+ return [];
}
var culture = _localizationManager.FindLanguageInfo(language);
@@ -369,14 +369,15 @@ namespace Emby.Server.Implementations.Library
return culture.ThreeLetterISOLanguageNames;
}
- return new string[] { language };
+ return [language];
}
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData.SubtitleStreamIndex.HasValue
&& user.RememberSubtitleSelections
- && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+ && user.SubtitleMode != SubtitlePlaybackMode.None
+ && allowRememberingSelection)
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
@@ -390,7 +391,7 @@ namespace Emby.Server.Implementations.Library
var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
- var audioLangage = defaultAudioIndex is null
+ var audioLanguage = defaultAudioIndex is null
? null
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
@@ -398,9 +399,9 @@ namespace Emby.Server.Implementations.Library
source.MediaStreams,
preferredSubs,
user.SubtitleMode,
- audioLangage);
+ audioLanguage);
- MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
+ MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLanguage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
@@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.Library
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
- public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
+ public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
var mediaType = item?.MediaType ?? MediaType.Video;
@@ -526,7 +527,7 @@ namespace Emby.Server.Implementations.Library
var item = request.ItemId.IsEmpty()
? null
: _libraryManager.GetItemById(request.ItemId);
- SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
+ SetDefaultAudioAndSubtitleStreamIndices(item, clone, user);
}
return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 6aef87c52..ea223e3ec 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -124,16 +124,16 @@ namespace Emby.Server.Implementations.Library
}
else if (mode == SubtitlePlaybackMode.Always)
{
- // always load the most suitable full subtitles
+ // Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
- // always load the most suitable full subtitles
+ // Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
}
- // load forced subs if we have found no suitable full subtitles
+ // Load forced subs if we have found no suitable full subtitles
var iterStreams = filteredStreams is null || filteredStreams.Count == 0
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 078f4ad21..a69a0f33f 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -13,7 +11,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Querying;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Library
@@ -27,33 +24,35 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
- public List<BaseItem> GetInstantMixFromSong(Audio item, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
- var list = new List<Audio>
+ var list = new List<BaseItem>
{
item
};
- return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList();
+ list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
+
+ return list;
}
/// <inheritdoc />
- public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromFolder(Folder item, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { BaseItemKind.Audio },
+ IncludeItemTypes = [BaseItemKind.Audio],
DtoOptions = dtoOptions
})
.Cast<Audio>()
@@ -64,12 +63,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@@ -86,27 +85,23 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { BaseItemKind.Audio },
-
- GenreIds = genreIds.ToArray(),
-
+ IncludeItemTypes = [BaseItemKind.Audio],
+ GenreIds = genreIds,
Limit = 200,
-
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
-
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)],
DtoOptions = dtoOptions
});
}
- public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
+ public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{
- return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
+ return GetInstantMixFromGenreIds([item.Id], user, dtoOptions);
}
if (item is Playlist playlist)
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index c4b6b3756..21e7079d8 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -31,8 +31,9 @@ namespace Emby.Server.Implementations.Library
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
- // Must be at least 3 characters after the attribute =, ], any character.
- var maxIndex = str.Length - attribute.Length - 3;
+ // Must be at least 3 characters after the attribute =, ], any character,
+ // then we offset it by 1, because we want the index and not length.
+ var maxIndex = str.Length - attribute.Length - 2;
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 7a61e2607..52be76217 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
- item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
+ item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 6cc04ea81..955055313 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- if (filename.IndexOf("[boxset]", StringComparison.OrdinalIgnoreCase) != -1 || args.ContainsFileSystemEntryByName("collection.xml"))
+ if (filename.Contains("[boxset]", StringComparison.OrdinalIgnoreCase) || args.ContainsFileSystemEntryByName("collection.xml"))
{
return new BoxSet
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a50435ae6..a03c1214d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.IO;
using System.Linq;
@@ -11,7 +9,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.LocalMetadata.Savers;
-using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Resolvers
{
@@ -20,11 +17,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
public class PlaylistResolver : GenericFolderResolver<Playlist>
{
- private CollectionType?[] _musicPlaylistCollectionTypes =
- {
+ private readonly CollectionType?[] _musicPlaylistCollectionTypes =
+ [
null,
CollectionType.music
- };
+ ];
/// <inheritdoc/>
protected override Playlist Resolve(ItemResolveArgs args)
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 020cb517d..7f3f8615e 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -29,7 +27,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{
- User user = null;
+ User? user = null;
if (!query.UserId.IsEmpty())
{
user = _userManager.GetUserById(query.UserId);
@@ -69,7 +67,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="user">The user.</param>
/// <returns>IEnumerable{SearchHintResult}.</returns>
/// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception>
- private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
+ private List<SearchHintInfo> GetSearchHints(SearchQuery query, User? user)
{
var searchTerm = query.SearchTerm;
@@ -78,7 +76,7 @@ namespace Emby.Server.Implementations.Library
searchTerm = searchTerm.Trim().RemoveDiacritics();
var excludeItemTypes = query.ExcludeItemTypes.ToList();
- var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<BaseItemKind>()).ToList();
+ var includeItemTypes = query.IncludeItemTypes.ToList();
excludeItemTypes.Add(BaseItemKind.Year);
excludeItemTypes.Add(BaseItemKind.Folder);
@@ -179,7 +177,7 @@ namespace Emby.Server.Implementations.Library
{
if (!searchQuery.ParentId.IsEmpty())
{
- searchQuery.AncestorIds = new[] { searchQuery.ParentId };
+ searchQuery.AncestorIds = [searchQuery.ParentId];
searchQuery.ParentId = Guid.Empty;
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 05af8d8a5..77643505e 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -125,5 +125,7 @@
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
+ "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
+ "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index c4d8c6947..b7633f77c 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -126,5 +126,7 @@
"External": "Extern",
"HearingImpaired": "Discapacitat auditiva",
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
- "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades."
+ "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
+ "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 1c7bc75b5..2fa1c19e3 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -126,5 +126,7 @@
"External": "Externí",
"HearingImpaired": "Sluchově postižení",
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
+ "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
+ "TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání."
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 092af34b6..b5e2c9b6b 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -126,5 +126,7 @@
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet",
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
- "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker."
+ "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
+ "TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 7a4c2067b..d8b2f828f 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -126,5 +126,7 @@
"External": "Extern",
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
- "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
+ "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken.",
+ "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 32bf89310..ff0c3d23d 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -126,5 +126,7 @@
"External": "External",
"HearingImpaired": "Hearing Impaired",
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
- "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
+ "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 496ecabd3..4ba31bee0 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -125,5 +125,7 @@
"TaskOptimizeDatabase": "Optimize database",
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
"TaskKeyframeExtractor": "Keyframe Extractor",
- "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time."
+ "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
+ "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index fe10be308..91e29d926 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -30,7 +30,7 @@
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
- "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
+ "LabelRunningTimeValue": "Duración: {0}",
"Latest": "Últimas",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index e0aff7954..db83d4b47 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -1,6 +1,6 @@
{
"Albums": "Albums",
- "AppDeviceValues": "Application : {0}, Appareil : {1}",
+ "AppDeviceValues": "Application : {0}, Appareil : {1}",
"Application": "Application",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
@@ -29,7 +29,7 @@
"Inherit": "Hériter",
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
- "LabelIpAddressValue": "Adresse IP : {0}",
+ "LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
@@ -126,5 +126,7 @@
"External": "Externe",
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
+ "TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 3f4dea523..a28352219 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -4,27 +4,27 @@
"HeaderNextUp": "इसके बाद",
"HeaderLiveTV": "लाइव टीवी",
"HeaderFavoriteSongs": "पसंदीदा गीत",
- "HeaderFavoriteShows": "पसंदीदा शोज",
- "HeaderFavoriteEpisodes": "पसंदीदा एपिसोड्स",
- "HeaderFavoriteArtists": "पसंदीदा कलाकारसमूह",
+ "HeaderFavoriteShows": "पसंदीदा शो",
+ "HeaderFavoriteEpisodes": "पसंदीदा प्रकरण",
+ "HeaderFavoriteArtists": "पसंदीदा कलाकार",
"HeaderFavoriteAlbums": "पसंदीदा एलबम्स",
- "HeaderContinueWatching": "देखते रहिए",
+ "HeaderContinueWatching": "देखना जारी रखें",
"HeaderAlbumArtists": "एल्बम कलाकार",
- "Genres": "शैली",
+ "Genres": "शैलियां",
"Forced": "बलपूर्वक",
- "Folders": "फ़ोल्डरें",
+ "Folders": "फ़ोल्डर",
"Favorites": "पसंदीदा",
"FailedLoginAttemptWithUserName": "{0} से लॉगिन असफल हुआ",
- "DeviceOnlineWithName": "{0} से संयोग हो गया है",
- "DeviceOfflineWithName": "{0} से संयोग विच्छिन्न हो गया है",
+ "DeviceOnlineWithName": "{0} कनेक्ट हो गया है",
+ "DeviceOfflineWithName": "{0} डिस्कनेक्ट हो गया है",
"Default": "प्राथमिक",
- "Collections": "संग्रहों",
- "ChapterNameValue": "अध्याय",
+ "Collections": "संग्रह",
+ "ChapterNameValue": "अध्याय {0}",
"Channels": "चैनल",
- "CameraImageUploadedFrom": "{0} से एक नया कैमरावाला चित्र अपलोड किया गया है",
- "Books": "पुस्तकों",
- "AuthenticationSucceededWithUserName": "सफलता से प्रमाणीकृत",
- "Artists": "कलाकारों",
+ "CameraImageUploadedFrom": "{0} से एक नया कैमरा छवि अपलोड की गई है",
+ "Books": "पुस्तकें",
+ "AuthenticationSucceededWithUserName": "{0} सफलतापूर्वक प्रमाणित किया गया",
+ "Artists": "कलाकार",
"Application": "एप्लिकेशन",
"AppDeviceValues": "एप: {0}, उपकरण: {1}",
"NotificationOptionPluginUninstalled": "प्लगइन अनइंस्टाल हो गया",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index a34bcc490..8d8311557 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -126,5 +126,7 @@
"External": "Esterno",
"HearingImpaired": "con problemi di udito",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate."
+ "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
+ "TaskCleanCollectionsAndPlaylists": "Ripulire le raccolte e le playlist",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle raccolte e dalle playlist che non esistono più."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index e7279994b..004ce68f5 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -126,5 +126,7 @@
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
- "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose."
+ "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
+ "TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių."
}
diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json
index 198f7540c..4cb4cdc75 100644
--- a/Emby.Server.Implementations/Localization/Core/my.json
+++ b/Emby.Server.Implementations/Localization/Core/my.json
@@ -48,7 +48,7 @@
"Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
"System": "စနစ်",
- "Sync": "ထပ်တူကျသည်။",
+ "Sync": "ချိန်ကိုက်မည်",
"SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
"Songs": "သီချင်းများ",
@@ -104,7 +104,7 @@
"HeaderFavoriteSongs": "အကြိုက်ဆုံးသီချင်းများ",
"HeaderFavoriteShows": "အကြိုက်ဆုံး ဇာတ်လမ်းတွဲများ",
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
- "HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
+ "HeaderFavoriteArtists": "အကြိုက်ဆုံး အနုပညာရှင်များ",
"HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
"HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
"HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
@@ -120,5 +120,11 @@
"AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
"Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
- "External": "ပြင်ပ"
+ "External": "ပြင်ပ",
+ "TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
+ "TaskCleanCollectionsAndPlaylistsDescription": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများမှ မရှိတော့သည်များကို ဖယ်ရှားမည်။",
+ "TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်",
+ "TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း",
+ "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်",
+ "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index be397f1b8..894d4b8ea 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Applicatie",
"Artists": "Artiesten",
- "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
+ "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
"Books": "Boeken",
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
@@ -107,7 +107,7 @@
"TaskRefreshLibraryDescription": "Scant de mediabibliotheek op nieuwe bestanden en vernieuwt de metadata.",
"TaskRefreshLibrary": "Mediabibliotheek scannen",
"TaskRefreshChapterImagesDescription": "Maakt voorbeeldafbeedingen aan voor video's met hoofdstukken.",
- "TaskRefreshChapterImages": "Hoofdstukafbeeldingen extraheren",
+ "TaskRefreshChapterImages": "Hoofdstukafbeeldingen uitpakken",
"TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
"TaskCleanCache": "Cache-map opschonen",
"TasksChannelsCategory": "Internetkanalen",
@@ -122,9 +122,11 @@
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
"TaskOptimizeDatabase": "Database optimaliseren",
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
- "TaskKeyframeExtractor": "Keyframe-uitpakker",
+ "TaskKeyframeExtractor": "Keyframes uitpakken",
"External": "Extern",
"HearingImpaired": "Slechthorend",
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
- "TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld."
+ "TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
+ "TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index bd572b744..64427b459 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -126,5 +126,7 @@
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
"HearingImpaired": "Niedosłyszący",
"TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
- "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
+ "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
+ "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 92ac2681e..dc96088ff 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -126,5 +126,7 @@
"External": "Externo",
"HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de truques",
- "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas."
+ "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
+ "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 103393a1e..de487488e 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -125,5 +125,7 @@
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
- "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
+ "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
+ "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 26d678a0c..3d3f88709 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -126,5 +126,7 @@
"External": "Внешние",
"HearingImpaired": "Для слабослышащих",
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
+ "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
+ "TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 43594a42e..905dba5ab 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -126,5 +126,7 @@
"External": "Externé",
"HearingImpaired": "Sluchovo postihnutí",
"TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
+ "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.",
+ "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú."
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index d7a627d12..059753957 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -126,5 +126,7 @@
"External": "Harici",
"HearingImpaired": "Duyma Engelli",
"TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur",
- "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur."
+ "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.",
+ "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 6f0dcfbe3..5f97d1ef9 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -83,7 +83,7 @@
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
"Songs": "Пісні",
- "Shows": "Телепередачі",
+ "Shows": "Серіали",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "{0} незавершено, збій",
@@ -125,5 +125,7 @@
"External": "Зовнішній",
"HearingImpaired": "З порушеннями слуху",
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
- "TaskRefreshTrickplayImages": "Створення Trickplay-зображень"
+ "TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
+ "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують."
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index e92752c5f..af9b54ad1 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -125,5 +125,7 @@
"External": "Bên ngoài",
"HearingImpaired": "Khiếm Thính",
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật."
+ "TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
+ "TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại."
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index b88d4eeaf..1f1458b6c 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -126,5 +126,7 @@
"External": "外部",
"HearingImpaired": "听力障碍",
"TaskRefreshTrickplayImages": "生成时间轴缩略图",
- "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
+ "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。",
+ "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
+ "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。"
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index aea8d6532..7a6cf9eff 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Playlists;
+using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PlaylistsNET.Content;
@@ -59,6 +60,11 @@ namespace Emby.Server.Implementations.Playlists
_appConfig = appConfig;
}
+ public Playlist GetPlaylistForUser(Guid playlistId, Guid userId)
+ {
+ return GetPlaylists(userId).Where(p => p.Id.Equals(playlistId)).FirstOrDefault();
+ }
+
public IEnumerable<Playlist> GetPlaylists(Guid userId)
{
var user = _userManager.GetUserById(userId);
@@ -66,61 +72,56 @@ namespace Emby.Server.Implementations.Playlists
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>();
}
- public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options)
+ public async Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request)
{
- var name = options.Name;
+ var name = request.Name;
var folderName = _fileSystem.GetValidFilename(name);
- var parentFolder = GetPlaylistsFolder(options.UserId);
+ var parentFolder = GetPlaylistsFolder(request.UserId);
if (parentFolder is null)
{
throw new ArgumentException(nameof(parentFolder));
}
- if (options.MediaType is null || options.MediaType == MediaType.Unknown)
+ if (request.MediaType is null || request.MediaType == MediaType.Unknown)
{
- foreach (var itemId in options.ItemIdList)
+ foreach (var itemId in request.ItemIdList)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- throw new ArgumentException("No item exists with the supplied Id");
- }
-
+ var item = _libraryManager.GetItemById(itemId) ?? throw new ArgumentException("No item exists with the supplied Id");
if (item.MediaType != MediaType.Unknown)
{
- options.MediaType = item.MediaType;
+ request.MediaType = item.MediaType;
}
else if (item is MusicArtist || item is MusicAlbum || item is MusicGenre)
{
- options.MediaType = MediaType.Audio;
+ request.MediaType = MediaType.Audio;
}
else if (item is Genre)
{
- options.MediaType = MediaType.Video;
+ request.MediaType = MediaType.Video;
}
else
{
if (item is Folder folder)
{
- options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
+ request.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
.Select(i => i.MediaType)
.FirstOrDefault(i => i != MediaType.Unknown);
}
}
- if (options.MediaType is not null && options.MediaType != MediaType.Unknown)
+ if (request.MediaType is not null && request.MediaType != MediaType.Unknown)
{
break;
}
}
}
- if (options.MediaType is null || options.MediaType == MediaType.Unknown)
+ if (request.MediaType is null || request.MediaType == MediaType.Unknown)
{
- options.MediaType = MediaType.Audio;
+ request.MediaType = MediaType.Audio;
}
- var user = _userManager.GetUserById(options.UserId);
+ var user = _userManager.GetUserById(request.UserId);
var path = Path.Combine(parentFolder.Path, folderName);
path = GetTargetPath(path);
@@ -133,19 +134,20 @@ namespace Emby.Server.Implementations.Playlists
{
Name = name,
Path = path,
- OwnerUserId = options.UserId,
- Shares = options.Shares ?? Array.Empty<Share>()
+ OwnerUserId = request.UserId,
+ Shares = request.Users ?? [],
+ OpenAccess = request.Public ?? false
};
- playlist.SetMediaType(options.MediaType);
+ playlist.SetMediaType(request.MediaType);
parentFolder.AddChild(playlist);
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false);
- if (options.ItemIdList.Count > 0)
+ if (request.ItemIdList.Count > 0)
{
- await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
+ await AddToPlaylistInternal(playlist.Id, request.ItemIdList, user, new DtoOptions(false)
{
EnableImages = true
}).ConfigureAwait(false);
@@ -160,7 +162,7 @@ namespace Emby.Server.Implementations.Playlists
}
}
- private string GetTargetPath(string path)
+ private static string GetTargetPath(string path)
{
while (Directory.Exists(path))
{
@@ -170,14 +172,14 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private List<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
+ private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
{
- var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null);
+ var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
}
- public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
+ public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
@@ -231,13 +233,8 @@ namespace Emby.Server.Implementations.Playlists
// Update the playlist in the repository
playlist.LinkedChildren = newLinkedChildren;
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- // Update the playlist on disk
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
// Refresh playlist metadata
_providerManager.QueueRefresh(
@@ -249,7 +246,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
+ public async Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
@@ -266,12 +263,7 @@ namespace Emby.Server.Implementations.Playlists
.Select(i => i.Item1)
.ToArray();
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
_providerManager.QueueRefresh(
playlist.Id,
@@ -313,14 +305,9 @@ namespace Emby.Server.Implementations.Playlists
newList.Insert(newIndex, item);
}
- playlist.LinkedChildren = newList.ToArray();
+ playlist.LinkedChildren = [.. newList];
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -430,8 +417,11 @@ namespace Emby.Server.Implementations.Playlists
}
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
- var playlist = new M3uPlaylist();
- playlist.IsExtended = true;
+ var playlist = new M3uPlaylist
+ {
+ IsExtended = true
+ };
+
foreach (var child in item.GetLinkedChildren())
{
var entry = new M3uPlaylistEntry()
@@ -481,7 +471,7 @@ namespace Emby.Server.Implementations.Playlists
}
}
- private string NormalizeItemPath(string playlistPath, string itemPath)
+ private static string NormalizeItemPath(string playlistPath, string itemPath)
{
return MakeRelativePath(Path.GetDirectoryName(playlistPath), itemPath);
}
@@ -537,16 +527,11 @@ namespace Emby.Server.Implementations.Playlists
{
// Update owner if shared
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray();
- if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid))
+ if (rankedShares.Length > 0)
{
- playlist.OwnerUserId = guid;
+ playlist.OwnerUserId = rankedShares[0].UserId;
playlist.Shares = rankedShares.Skip(1).ToArray();
- await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
-
- if (playlist.IsFile)
- {
- SavePlaylistFile(playlist);
- }
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
}
else if (!playlist.OpenAccess)
{
@@ -563,5 +548,76 @@ namespace Emby.Server.Implementations.Playlists
}
}
}
+
+ public async Task UpdatePlaylist(PlaylistUpdateRequest request)
+ {
+ var playlist = GetPlaylistForUser(request.Id, request.UserId);
+
+ if (request.Ids is not null)
+ {
+ playlist.LinkedChildren = [];
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+
+ var user = _userManager.GetUserById(request.UserId);
+ await AddToPlaylistInternal(request.Id, request.Ids, user, new DtoOptions(false)
+ {
+ EnableImages = true
+ }).ConfigureAwait(false);
+
+ playlist = GetPlaylistForUser(request.Id, request.UserId);
+ }
+
+ if (request.Name is not null)
+ {
+ playlist.Name = request.Name;
+ }
+
+ if (request.Users is not null)
+ {
+ playlist.Shares = request.Users;
+ }
+
+ if (request.Public is not null)
+ {
+ playlist.OpenAccess = request.Public.Value;
+ }
+
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ public async Task AddUserToShares(PlaylistUserUpdateRequest request)
+ {
+ var userId = request.UserId;
+ var playlist = GetPlaylistForUser(request.Id, userId);
+ var shares = playlist.Shares.ToList();
+ var existingUserShare = shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ if (existingUserShare is not null)
+ {
+ shares.Remove(existingUserShare);
+ }
+
+ shares.Add(new PlaylistUserPermissions(userId, request.CanEdit ?? false));
+ playlist.Shares = shares;
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ public async Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share)
+ {
+ var playlist = GetPlaylistForUser(playlistId, userId);
+ var shares = playlist.Shares.ToList();
+ shares.Remove(share);
+ playlist.Shares = shares;
+ await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
+ }
+
+ private async Task UpdatePlaylistInternal(Playlist playlist)
+ {
+ await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+
+ if (playlist.IsFile)
+ {
+ SavePlaylistFile(playlist);
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 40b3b0339..06798628f 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -159,10 +159,7 @@ namespace Emby.Server.Implementations.Session
private void CheckDisposed()
{
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
+ ObjectDisposedException.ThrowIf(_disposed, this);
}
private void OnSessionStarted(SessionInfo info)
@@ -456,8 +453,8 @@ namespace Emby.Server.Implementations.Session
if (!_activeConnections.TryGetValue(key, out var sessionInfo))
{
- _activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
- sessionInfo = _activeConnections[key];
+ sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ _activeConnections[key] = sessionInfo;
}
sessionInfo.UserId = user?.Id ?? Guid.Empty;
@@ -614,9 +611,6 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug(ex, "Error calling OnPlaybackStopped");
}
}
-
- playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
- .ToList();
}
else
{
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 65c8599e7..59185cdb7 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -9,37 +8,35 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class AlbumArtistComparer.
+ /// Allows comparing artists of albums. Only the first artist of each album is considered.
/// </summary>
public class AlbumArtistComparer : IBaseItemComparer
{
/// <summary>
- /// Gets the name.
+ /// Gets the item type this comparer compares.
/// </summary>
- /// <value>The name.</value>
public ItemSortBy Type => ItemSortBy.AlbumArtist;
/// <summary>
- /// Compares the specified x.
+ /// Compares the specified arguments on their primary artist.
/// </summary>
- /// <param name="x">The x.</param>
- /// <param name="y">The y.</param>
- /// <returns>System.Int32.</returns>
+ /// <param name="x">First item to compare.</param>
+ /// <param name="y">Second item to compare.</param>
+ /// <returns>Zero if equal, else negative or positive number to indicate order.</returns>
public int Compare(BaseItem? x, BaseItem? y)
{
- return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
+ return string.Compare(GetFirstAlbumArtist(x), GetFirstAlbumArtist(y), StringComparison.OrdinalIgnoreCase);
}
- /// <summary>
- /// Gets the value.
- /// </summary>
- /// <param name="x">The x.</param>
- /// <returns>System.String.</returns>
- private static string? GetValue(BaseItem? x)
+ private static string? GetFirstAlbumArtist(BaseItem? x)
{
- var audio = x as IHasAlbumArtist;
+ if (x is IHasAlbumArtist audio
+ && audio.AlbumArtists.Count != 0)
+ {
+ return audio.AlbumArtists[0];
+ }
- return audio?.AlbumArtists.FirstOrDefault();
+ return null;
}
}
}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 965b7e7e6..2b6b2a82c 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,10 +1,6 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using Jellyfin.Api.Extensions;
-using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
@@ -15,19 +11,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement>
{
private readonly IConfigurationManager _configurationManager;
- private readonly IUserManager _userManager;
/// <summary>
/// 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>
- public FirstTimeSetupHandler(
- IConfigurationManager configurationManager,
- IUserManager userManager)
+ public FirstTimeSetupHandler(IConfigurationManager configurationManager)
{
_configurationManager = configurationManager;
- _userManager = userManager;
}
/// <inheritdoc />
@@ -36,37 +27,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(requirement);
- return Task.CompletedTask;
}
-
- var contextUser = context.User;
- if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator))
+ else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
{
context.Fail();
- return Task.CompletedTask;
}
-
- var userId = contextUser.GetUserId();
- if (userId.IsEmpty())
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
- if (!requirement.ValidateParentalSchedule)
- {
- context.Succeed(requirement);
- return Task.CompletedTask;
- }
-
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- throw new ResourceNotFoundException();
- }
-
- if (user.IsParentalScheduleAllowed())
+ else
{
+ // Any user-specific checks are handled in the DefaultAuthorizationHandler.
context.Succeed(requirement);
}
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index cd09d2bfa..72be55513 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -132,8 +132,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -261,12 +261,12 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -296,8 +296,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 8db22f7eb..abe8bec2d 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -125,12 +125,15 @@ public class ConfigurationController : BaseJellyfinApiController
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
/// <response code="204">Media encoder path updated.</response>
/// <returns>Status.</returns>
+ [Obsolete("This endpoint is obsolete.")]
+ [ApiExplorerSettings(IgnoreApi = true)]
[HttpPost("MediaEncoder/Path")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
{
- _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
+ // API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES
+ // _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 590cdc33f..49fc2f3d7 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -163,18 +163,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -204,8 +204,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -406,12 +406,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -443,8 +443,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -577,12 +577,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -613,8 +613,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -742,12 +742,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -779,8 +779,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -909,12 +909,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -945,8 +945,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1085,12 +1085,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1122,8 +1122,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1265,12 +1265,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1301,8 +1301,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1604,7 +1604,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Path.GetFileNameWithoutExtension(outputPath));
}
- var hlsArguments = GetHlsArguments(isEventPlaylist, state.SegmentLength);
+ var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
return string.Format(
CultureInfo.InvariantCulture,
@@ -1626,33 +1626,6 @@ public class DynamicHlsController : BaseJellyfinApiController
}
/// <summary>
- /// Gets the HLS arguments for transcoding.
- /// </summary>
- /// <returns>The command line arguments for HLS transcoding.</returns>
- private string GetHlsArguments(bool isEventPlaylist, int segmentLength)
- {
- var enableThrottling = _encodingOptions.EnableThrottling;
- var enableSegmentDeletion = _encodingOptions.EnableSegmentDeletion;
-
- // Only enable segment deletion when throttling is enabled
- if (enableThrottling && enableSegmentDeletion)
- {
- // Store enough segments for configured seconds of playback; this needs to be above throttling settings
- var segmentCount = _encodingOptions.SegmentKeepSeconds / segmentLength;
-
- _logger.LogDebug("Using throttling and segment deletion, keeping {0} segments", segmentCount);
-
- return string.Format(CultureInfo.InvariantCulture, "-hls_list_size {0} -hls_flags delete_segments", segmentCount.ToString(CultureInfo.InvariantCulture));
- }
- else
- {
- _logger.LogDebug("Using normal playback, is event playlist? {0}", isEventPlaylist);
-
- return string.Format(CultureInfo.InvariantCulture, "-hls_playlist_type {0} -hls_list_size 0", isEventPlaylist ? "event" : "vod");
- }
- }
-
- /// <summary>
/// Gets the audio arguments for transcoding.
/// </summary>
/// <param name="state">The <see cref="StreamState"/>.</param>
@@ -1802,11 +1775,17 @@ public class DynamicHlsController : BaseJellyfinApiController
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
+ var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
+ var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
+
if (EncodingHelper.IsCopyCodec(codec)
- && (state.VideoStream.VideoRangeType == VideoRangeType.DOVI
- || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
+ && ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
+ || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
+ || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
+ || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
{
// Prefer dvh1 to dvhe
args += " -tag:v:0 dvh1 -strict -2";
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 984dc7789..360389d29 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -520,7 +520,11 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
{
- var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
+ var items = _libraryManager.GetUserRootFolder().Children
+ .Concat(_libraryManager.RootFolder.VirtualChildren)
+ .Where(i => _libraryManager.GetLibraryOptions(i).Enabled)
+ .OrderBy(i => i.SortName)
+ .ToList();
if (isHidden.HasValue)
{
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 0e7c3f155..1100f85cf 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -92,29 +92,267 @@ public class PlaylistsController : BaseJellyfinApiController
Name = name ?? createPlaylistRequest?.Name,
ItemIdList = ids,
UserId = userId.Value,
- MediaType = mediaType ?? createPlaylistRequest?.MediaType
+ MediaType = mediaType ?? createPlaylistRequest?.MediaType,
+ Users = createPlaylistRequest?.Users.ToArray() ?? [],
+ Public = createPlaylistRequest?.IsPublic
}).ConfigureAwait(false);
return result;
}
/// <summary>
+ /// Updates a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="updatePlaylistRequest">The <see cref="UpdatePlaylistDto"/> id.</param>
+ /// <response code="204">Playlist updated.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to update a playlist.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpPost("{playlistId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdatePlaylist(
+ [FromRoute, Required] Guid playlistId,
+ [FromBody, Required] UpdatePlaylistDto updatePlaylistRequest)
+ {
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ await _playlistManager.UpdatePlaylist(new PlaylistUpdateRequest
+ {
+ UserId = callingUserId,
+ Id = playlistId,
+ Name = updatePlaylistRequest.Name,
+ Ids = updatePlaylistRequest.Ids,
+ Users = updatePlaylistRequest.Users,
+ Public = updatePlaylistRequest.IsPublic
+ }).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Get a playlist's users.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <response code="200">Found shares.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A list of <see cref="PlaylistUserPermissions"/> objects.
+ /// </returns>
+ [HttpGet("{playlistId}/Users")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IReadOnlyList<PlaylistUserPermissions>> GetPlaylistUsers(
+ [FromRoute, Required] Guid playlistId)
+ {
+ var userId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(userId);
+
+ return isPermitted ? playlist.Shares.ToList() : Forbid();
+ }
+
+ /// <summary>
+ /// Get a playlist user.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">User permission found.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// <see cref="PlaylistUserPermissions"/>.
+ /// </returns>
+ [HttpGet("{playlistId}/Users/{userId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<PlaylistUserPermissions?> GetPlaylistUser(
+ [FromRoute, Required] Guid playlistId,
+ [FromRoute, Required] Guid userId)
+ {
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var userPermission = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId))
+ || userId.Equals(callingUserId);
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ if (userPermission is not null)
+ {
+ return userPermission;
+ }
+
+ return NotFound("User permissions not found");
+ }
+
+ /// <summary>
+ /// Modify a user of a playlist's users.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="updatePlaylistUserRequest">The <see cref="UpdatePlaylistUserDto"/>.</param>
+ /// <response code="204">User's permissions modified.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to modify an user's playlist permissions.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpPost("{playlistId}/Users/{userId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdatePlaylistUser(
+ [FromRoute, Required] Guid playlistId,
+ [FromRoute, Required] Guid userId,
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow), Required] UpdatePlaylistUserDto updatePlaylistUserRequest)
+ {
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId);
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest
+ {
+ Id = playlistId,
+ UserId = userId,
+ CanEdit = updatePlaylistUserRequest.CanEdit
+ }).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Remove a user from a playlist's users.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="204">User permissions removed from playlist.</response>
+ /// <response code="401">Unauthorized access.</response>
+ /// <response code="404">No playlist or user permissions found.</response>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to delete a user from a playlist's shares.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpDelete("{playlistId}/Users/{userId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> RemoveUserFromPlaylist(
+ [FromRoute, Required] Guid playlistId,
+ [FromRoute, Required] Guid userId)
+ {
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ if (share is null)
+ {
+ return NotFound("User permissions not found");
+ }
+
+ await _playlistManager.RemoveUserFromShares(playlistId, callingUserId, share).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
/// Adds items to a playlist.
/// </summary>
/// <param name="playlistId">The playlist id.</param>
/// <param name="ids">Item id, comma delimited.</param>
/// <param name="userId">The userId.</param>
/// <response code="204">Items added to playlist.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToPlaylist(
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(userId.Value)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId.Value));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
return NoContent();
}
@@ -125,14 +363,34 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="newIndex">The new index.</param>
/// <response code="204">Item moved to new index.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> MoveItem(
[FromRoute, Required] string playlistId,
[FromRoute, Required] string itemId,
[FromRoute, Required] int newIndex)
{
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
return NoContent();
}
@@ -143,14 +401,34 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="playlistId">The playlist id.</param>
/// <param name="entryIds">The item ids, comma delimited.</param>
/// <response code="204">Items removed.</response>
+ /// <response code="403">Access forbidden.</response>
+ /// <response code="404">Playlist not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromPlaylist(
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> RemoveItemFromPlaylist(
[FromRoute, Required] string playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
{
- await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
+ var callingUserId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
+
+ await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
return NoContent();
}
@@ -167,10 +445,12 @@ public class PlaylistsController : BaseJellyfinApiController
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Original playlist returned.</response>
+ /// <response code="404">Access forbidden.</response>
/// <response code="404">Playlist not found.</response>
/// <returns>The original playlist items.</returns>
[HttpGet("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
[FromRoute, Required] Guid playlistId,
@@ -184,10 +464,19 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
- var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
if (playlist is null)
{
- return NotFound();
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OpenAccess
+ || playlist.OwnerUserId.Equals(userId.Value)
+ || playlist.Shares.Any(s => s.UserId.Equals(userId.Value));
+
+ if (!isPermitted)
+ {
+ return Forbid();
}
var user = userId.IsNullOrEmpty()
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index cc2a630e1..e2c5486d9 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -165,7 +165,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/{subtitleId}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesFile("text/*")]
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 4c3ef2c7f..db78e9946 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -92,13 +92,13 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
- [FromQuery] string? transcodingContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
[FromQuery] MediaStreamProtocol? transcodingProtocol,
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
@@ -157,7 +157,7 @@ public class UniversalAudioController : BaseJellyfinApiController
}
var isStatic = mediaSource.SupportsDirectStream;
- if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.Hls)
+ if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
{
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
// ffmpeg option -> file extension
@@ -268,7 +268,7 @@ public class UniversalAudioController : BaseJellyfinApiController
Context = EncodingContext.Streaming,
Container = transcodingContainer ?? "mp3",
AudioCodec = audioCodec ?? "mp3",
- Protocol = transcodingProtocol ?? MediaStreamProtocol.Http,
+ Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index b3029d6fa..380120032 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -311,18 +311,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -354,8 +354,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -555,12 +555,12 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -592,8 +592,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 5eec1b0ca..ec67b4c1b 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -192,7 +192,7 @@ public static class HlsCodecStringHelpers
/// <returns>The AV1 codec string.</returns>
public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth)
{
- // https://aomedia.org/av1/specification/annex-a/
+ // https://aomediacodec.github.io/av1-isobmff/#codecsparam
// FORMAT: [codecTag].[profile].[level][tier].[bitDepth]
StringBuilder result = new StringBuilder("av01", 13);
@@ -214,8 +214,7 @@ public static class HlsCodecStringHelpers
result.Append(".0");
}
- if (level <= 0
- || level > 31)
+ if (level is <= 0 or > 31)
{
// Default to the maximum defined level 6.3
level = 19;
@@ -230,7 +229,8 @@ public static class HlsCodecStringHelpers
}
result.Append('.')
- .Append(level)
+ // Needed to pad it double digits; otherwise, browsers will reject the stream.
+ .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", level)
.Append(tierFlag ? 'H' : 'M');
string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture);
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index bdc488871..3cbdd031a 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
+using MediaBrowser.Model.Entities;
namespace Jellyfin.Api.Models.PlaylistDtos;
@@ -14,13 +15,13 @@ public class CreatePlaylistDto
/// <summary>
/// Gets or sets the name of the new playlist.
/// </summary>
- public string? Name { get; set; }
+ public required string Name { get; set; }
/// <summary>
/// Gets or sets item ids to add to the playlist.
/// </summary>
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
+ public IReadOnlyList<Guid> Ids { get; set; } = [];
/// <summary>
/// Gets or sets the user id.
@@ -31,4 +32,14 @@ public class CreatePlaylistDto
/// Gets or sets the media type.
/// </summary>
public MediaType? MediaType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist users.
+ /// </summary>
+ public IReadOnlyList<PlaylistUserPermissions> Users { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool IsPublic { get; set; } = true;
}
diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
new file mode 100644
index 000000000..80e20995c
--- /dev/null
+++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Jellyfin.Extensions.Json.Converters;
+using MediaBrowser.Model.Entities;
+
+namespace Jellyfin.Api.Models.PlaylistDtos;
+
+/// <summary>
+/// Update existing playlist dto. Fields set to `null` will not be updated and keep their current values.
+/// </summary>
+public class UpdatePlaylistDto
+{
+ /// <summary>
+ /// Gets or sets the name of the new playlist.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets item ids of the playlist.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ public IReadOnlyList<Guid>? Ids { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist users.
+ /// </summary>
+ public IReadOnlyList<PlaylistUserPermissions>? Users { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool? IsPublic { get; set; }
+}
diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs
new file mode 100644
index 000000000..60467b5e7
--- /dev/null
+++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs
@@ -0,0 +1,12 @@
+namespace Jellyfin.Api.Models.PlaylistDtos;
+
+/// <summary>
+/// Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values.
+/// </summary>
+public class UpdatePlaylistUserDto
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the user can edit the playlist.
+ /// </summary>
+ public bool? CanEdit { get; set; }
+}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index 12ce19368..b72dcff88 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -55,12 +55,12 @@ public class ClientCapabilitiesDto
// TODO: Remove after 10.9
[Obsolete("Unused")]
[DefaultValue(false)]
- public bool? SupportsContentUploading { get; set; }
+ public bool? SupportsContentUploading { get; set; } = false;
// TODO: Remove after 10.9
[Obsolete("Unused")]
[DefaultValue(false)]
- public bool? SupportsSync { get; set; }
+ public bool? SupportsSync { get; set; } = false;
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
/// <summary>
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index ba228cb00..99516e938 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -20,6 +20,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
/// </summary>
private readonly IActivityManager _activityManager;
+ private bool _disposed;
+
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class.
/// </summary>
@@ -51,14 +53,15 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
}
/// <inheritdoc />
- protected override void Dispose(bool dispose)
+ protected override async ValueTask DisposeAsyncCore()
{
- if (dispose)
+ if (!_disposed)
{
_activityManager.EntryCreated -= OnEntryCreated;
+ _disposed = true;
}
- base.Dispose(dispose);
+ await base.DisposeAsyncCore().ConfigureAwait(false);
}
/// <summary>
@@ -75,8 +78,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
base.Start(message);
}
- private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
+ private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 37c108d5a..dd9286210 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -20,6 +20,8 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn
/// <value>The task manager.</value>
private readonly ITaskManager _taskManager;
+ private bool _disposed;
+
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class.
/// </summary>
@@ -56,31 +58,32 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn
}
/// <inheritdoc />
- protected override void Dispose(bool dispose)
+ protected override async ValueTask DisposeAsyncCore()
{
- if (dispose)
+ if (!_disposed)
{
_taskManager.TaskExecuting -= OnTaskExecuting;
_taskManager.TaskCompleted -= OnTaskCompleted;
+ _disposed = true;
}
- base.Dispose(dispose);
+ await base.DisposeAsyncCore().ConfigureAwait(false);
}
- private async void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
+ private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e)
{
e.Task.TaskProgress -= OnTaskProgress;
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
- private async void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
+ private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
e.Argument.TaskProgress += OnTaskProgress;
}
- private async void OnTaskProgress(object? sender, GenericEventArgs<double> e)
+ private void OnTaskProgress(object? sender, GenericEventArgs<double> e)
{
- await SendData(false).ConfigureAwait(false);
+ SendData(false);
}
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 3c2b86142..a6cfe4d56 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -16,6 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners;
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
{
private readonly ISessionManager _sessionManager;
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
@@ -55,9 +56,9 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
}
/// <inheritdoc />
- protected override void Dispose(bool dispose)
+ protected override async ValueTask DisposeAsyncCore()
{
- if (dispose)
+ if (!_disposed)
{
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
@@ -66,9 +67,10 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
_sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
_sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
_sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ _disposed = true;
}
- base.Dispose(dispose);
+ await base.DisposeAsyncCore().ConfigureAwait(false);
}
/// <summary>
@@ -85,38 +87,38 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
base.Start(message);
}
- private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
+ private void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
{
- await SendData(false).ConfigureAwait(false);
+ SendData(false);
}
- private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
+ private void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
- private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
+ private void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
- await SendData(!e.IsAutomated).ConfigureAwait(false);
+ SendData(!e.IsAutomated);
}
- private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
+ private void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
- private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
+ private void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
- private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
+ private void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
- private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
+ private void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e)
{
- await SendData(true).ConfigureAwait(false);
+ SendData(true);
}
}
diff --git a/Jellyfin.Data/Enums/MediaStreamProtocol.cs b/Jellyfin.Data/Enums/MediaStreamProtocol.cs
index 965edd6c1..844dc95c1 100644
--- a/Jellyfin.Data/Enums/MediaStreamProtocol.cs
+++ b/Jellyfin.Data/Enums/MediaStreamProtocol.cs
@@ -1,20 +1,22 @@
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
using System.ComponentModel;
namespace Jellyfin.Data.Enums;
/// <summary>
/// Media streaming protocol.
+/// Lowercase for backwards compatibility.
/// </summary>
-[DefaultValue(Http)]
+[DefaultValue(http)]
public enum MediaStreamProtocol
{
/// <summary>
/// HTTP.
/// </summary>
- Http = 0,
+ http = 0,
/// <summary>
/// HTTP Live Streaming.
/// </summary>
- Hls = 1
+ hls = 1
}
diff --git a/Jellyfin.Data/Enums/VideoRangeType.cs b/Jellyfin.Data/Enums/VideoRangeType.cs
index 7ac7bc20a..853c2c73d 100644
--- a/Jellyfin.Data/Enums/VideoRangeType.cs
+++ b/Jellyfin.Data/Enums/VideoRangeType.cs
@@ -26,11 +26,26 @@ public enum VideoRangeType
HLG,
/// <summary>
- /// Dolby Vision video range type (12bit).
+ /// Dolby Vision video range type (10bit encoded / 12bit remapped).
/// </summary>
DOVI,
/// <summary>
+ /// Dolby Vision with HDR10 video range fallback (10bit).
+ /// </summary>
+ DOVIWithHDR10,
+
+ /// <summary>
+ /// Dolby Vision with HLG video range fallback (10bit).
+ /// </summary>
+ DOVIWithHLG,
+
+ /// <summary>
+ /// Dolby Vision with SDR video range fallback (8bit / 10bit).
+ /// </summary>
+ DOVIWithSDR,
+
+ /// <summary>
/// HDR10+ video range type (10bit to 16bit).
/// </summary>
HDR10Plus
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index 095bc9ed3..fed5dab69 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -141,6 +141,7 @@ public class TrickplayManager : ITrickplayManager
width,
TimeSpan.FromMilliseconds(options.Interval),
options.EnableHwAcceleration,
+ options.EnableHwEncoding,
options.ProcessThreads,
options.Qscale,
options.ProcessPriority,
diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
index fb9f6d0a6..fb0bd817c 100644
--- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
+++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs
@@ -1,91 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
+using Jellyfin.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
-namespace Jellyfin.Server.Filters
+namespace Jellyfin.Server.Filters;
+
+/// <summary>
+/// Security requirement operation filter.
+/// </summary>
+public class SecurityRequirementsOperationFilter : IOperationFilter
{
+ private const string DefaultAuthPolicy = "DefaultAuthorization";
+ private static readonly Type _attributeType = typeof(AuthorizeAttribute);
+
+ private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider;
+
/// <summary>
- /// Security requirement operation filter.
+ /// Initializes a new instance of the <see cref="SecurityRequirementsOperationFilter"/> class.
/// </summary>
- public class SecurityRequirementsOperationFilter : IOperationFilter
+ /// <param name="authorizationPolicyProvider">The authorization policy provider.</param>
+ public SecurityRequirementsOperationFilter(IAuthorizationPolicyProvider authorizationPolicyProvider)
{
- /// <inheritdoc />
- public void Apply(OpenApiOperation operation, OperationFilterContext context)
- {
- var requiredScopes = new List<string>();
+ _authorizationPolicyProvider = authorizationPolicyProvider;
+ }
- var requiresAuth = false;
- // Add all method scopes.
- foreach (var attribute in context.MethodInfo.GetCustomAttributes(true))
- {
- if (attribute is not AuthorizeAttribute authorizeAttribute)
- {
- continue;
- }
+ /// <inheritdoc />
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ var requiredScopes = new List<string>();
- requiresAuth = true;
- if (authorizeAttribute.Policy is not null
- && !requiredScopes.Contains(authorizeAttribute.Policy, StringComparer.Ordinal))
- {
- requiredScopes.Add(authorizeAttribute.Policy);
- }
+ var requiresAuth = false;
+ // Add all method scopes.
+ foreach (var authorizeAttribute in context.MethodInfo.GetCustomAttributes(_attributeType, true).Cast<AuthorizeAttribute>())
+ {
+ requiresAuth = true;
+ var policy = authorizeAttribute.Policy ?? DefaultAuthPolicy;
+ if (!requiredScopes.Contains(policy, StringComparer.Ordinal))
+ {
+ requiredScopes.Add(policy);
}
+ }
- // Add controller scopes if any.
- var controllerAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true);
- if (controllerAttributes is not null)
+ // Add controller scopes if any.
+ var controllerAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(_attributeType, true).Cast<AuthorizeAttribute>();
+ if (controllerAttributes is not null)
+ {
+ foreach (var authorizeAttribute in controllerAttributes)
{
- foreach (var attribute in controllerAttributes)
+ requiresAuth = true;
+ var policy = authorizeAttribute.Policy ?? DefaultAuthPolicy;
+ if (!requiredScopes.Contains(policy, StringComparer.Ordinal))
{
- 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);
- }
+ requiredScopes.Add(policy);
}
}
+ }
- if (!requiresAuth)
- {
- return;
- }
+ if (!requiresAuth)
+ {
+ return;
+ }
- if (!operation.Responses.ContainsKey("401"))
- {
- operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
- }
+ if (!operation.Responses.ContainsKey("401"))
+ {
+ operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
+ }
- if (!operation.Responses.ContainsKey("403"))
- {
- operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
- }
+ if (!operation.Responses.ContainsKey("403"))
+ {
+ operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
+ }
- var scheme = new OpenApiSecurityScheme
+ 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>
+ // Add DefaultAuthorization scope to any endpoint that has a policy with a requirement that is a subset of DefaultAuthorization.
+ if (!requiredScopes.Contains(DefaultAuthPolicy.AsSpan(), StringComparison.Ordinal))
+ {
+ foreach (var scope in requiredScopes)
{
- new OpenApiSecurityRequirement
+ var authorizationPolicy = _authorizationPolicyProvider.GetPolicyAsync(scope).GetAwaiter().GetResult();
+ if (authorizationPolicy is not null
+ && authorizationPolicy.Requirements.Any(r => r is DefaultAuthorizationRequirement))
{
- [scheme] = requiredScopes
+ requiredScopes.Add(DefaultAuthPolicy);
+ break;
}
- };
+ }
}
+
+ operation.Security = [new OpenApiSecurityRequirement { [scheme] = requiredScopes }];
}
}
diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs
index 66d393dec..5311a30e4 100644
--- a/Jellyfin.Server/Helpers/StartupHelpers.cs
+++ b/Jellyfin.Server/Helpers/StartupHelpers.cs
@@ -57,6 +57,9 @@ public static class StartupHelpers
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
+ logger.LogInformation("Log directory path: {LogDirectoryPath}", appPaths.LogDirectoryPath);
+ logger.LogInformation("Config directory path: {ConfigurationDirectoryPath}", appPaths.ConfigurationDirectoryPath);
+ logger.LogInformation("Cache path: {CachePath}", appPaths.CachePath);
logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index cf3182003..3655a610d 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -54,12 +54,12 @@ internal class FixPlaylistOwner : IMigrationRoutine
foreach (var playlist in playlists)
{
var shares = playlist.Shares;
- if (shares.Length > 0)
+ if (shares.Count > 0)
{
var firstEditShare = shares.First(x => x.CanEdit);
- if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid))
+ if (firstEditShare is not null)
{
- playlist.OwnerUserId = guid;
+ playlist.OwnerUserId = firstEditShare.UserId;
playlist.Shares = shares.Where(x => x != firstEditShare).ToArray();
playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
_playlistManager.SavePlaylistFile(playlist);
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index c70ef1719..fd7696906 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -12,6 +12,7 @@ using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -139,7 +140,15 @@ namespace Jellyfin.Server
host = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
- .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
+ .ConfigureWebHostDefaults(webHostBuilder =>
+ {
+ webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger);
+ if (bool.TryParse(Environment.GetEnvironmentVariable("JELLYFIN_ENABLE_IIS"), out var iisEnabled) && iisEnabled)
+ {
+ _logger.LogCritical("UNSUPPORTED HOSTING ENVIRONMENT Microsoft Internet Information Services. The option to run Jellyfin on IIS is an unsupported and untested feature. Only use at your own discretion.");
+ webHostBuilder.UseIIS();
+ }
+ })
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
.UseSerilog()
.Build();
diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
index 9fae43033..7d5207c34 100644
--- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs
+++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Channels
@@ -11,6 +9,6 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>System.String.</returns>
- string GetCacheKey(string userId);
+ string? GetCacheKey(string? userId);
}
}
diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
deleted file mode 100644
index b87943a6e..000000000
--- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Channels
-{
- public interface ISearchableChannel
- {
- /// <summary>
- /// Searches the specified search term.
- /// </summary>
- /// <param name="searchInfo">The search information.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
- Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
index 8ad93387e..8ecc68bab 100644
--- a/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
+++ b/MediaBrowser.Controller/Channels/ISupportsLatestMedia.cs
@@ -1,6 +1,4 @@
-#nullable disable
-
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System.Collections.Generic;
using System.Threading;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index ddcc994a0..5f9840b1b 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -62,7 +62,9 @@ namespace MediaBrowser.Controller.Entities
".edl",
".bif",
".smi",
- ".ttml"
+ ".ttml",
+ ".lrc",
+ ".elrc"
};
/// <summary>
@@ -831,7 +833,7 @@ namespace MediaBrowser.Controller.Entities
return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
}
- public bool CanDelete(User user)
+ public virtual bool CanDelete(User user)
{
var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
@@ -962,7 +964,13 @@ namespace MediaBrowser.Controller.Entities
AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
- return builder.ToString().RemoveDiacritics();
+ var result = builder.ToString().RemoveDiacritics();
+ if (!result.All(char.IsAscii))
+ {
+ result = result.Transliterated();
+ }
+
+ return result;
}
public BaseItem GetParent()
@@ -1578,18 +1586,24 @@ namespace MediaBrowser.Controller.Entities
list.AddRange(parent.Tags);
}
+ foreach (var folder in LibraryManager.GetCollectionFolders(this))
+ {
+ list.AddRange(folder.Tags);
+ }
+
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private bool IsVisibleViaTags(User user)
{
- if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ var allTags = GetInheritedTags();
+ if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
- if (allowedTagsPreference.Any() && !allowedTagsPreference.Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 992bb19bb..676a47c88 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
@@ -95,6 +96,16 @@ namespace MediaBrowser.Controller.Entities
return GetLibraryOptions(Path);
}
+ public override bool IsVisible(User user)
+ {
+ if (GetLibraryOptions().Enabled)
+ {
+ return base.IsVisible(user);
+ }
+
+ return false;
+ }
+
private static LibraryOptions LoadLibraryOptions(string path)
{
try
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 1f13c833b..8bfcf5dee 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -331,8 +331,25 @@ namespace MediaBrowser.Controller.Entities
}
}
+ private static bool IsLibraryFolderAccessible(IDirectoryService directoryService, BaseItem item)
+ {
+ // For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
+ if (item.IsTopParent && !directoryService.IsAccessible(item.ContainingFolderPath))
+ {
+ Logger.LogWarning("Library folder {LibraryFolderPath} is inaccessible or empty, skipping", item.ContainingFolderPath);
+ return false;
+ }
+
+ return true;
+ }
+
private async Task ValidateChildrenInternal2(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
+ if (!IsLibraryFolderAccessible(directoryService, this))
+ {
+ return;
+ }
+
cancellationToken.ThrowIfCancellationRequested();
var validChildren = new List<BaseItem>();
@@ -369,6 +386,11 @@ namespace MediaBrowser.Controller.Entities
foreach (var child in nonCachedChildren)
{
+ if (!IsLibraryFolderAccessible(directoryService, child))
+ {
+ continue;
+ }
+
if (currentChildren.TryGetValue(child.Id, out BaseItem currentChild))
{
validChildren.Add(currentChild);
@@ -392,8 +414,8 @@ namespace MediaBrowser.Controller.Entities
validChildren.Add(child);
}
- // If any items were added or removed....
- if (newItems.Count > 0 || currentChildren.Count != validChildren.Count)
+ // If it's an AggregateFolder, don't remove
+ if (!IsRoot && currentChildren.Count != validChildren.Count)
{
// That's all the new and changed ones - now see if there are any that are missing
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
@@ -408,7 +430,10 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
}
}
+ }
+ if (newItems.Count > 0)
+ {
LibraryManager.CreateItems(newItems, this, cancellationToken);
}
}
@@ -435,15 +460,7 @@ namespace MediaBrowser.Controller.Entities
progress.Report(percent);
- // TODO: this is sometimes being called after the refresh has completed.
- try
- {
- ProviderManager.OnRefreshProgress(folder, percent);
- }
- catch (InvalidOperationException e)
- {
- Logger.LogError(e, "Error refreshing folder");
- }
+ ProviderManager.OnRefreshProgress(folder, percent);
});
if (validChildrenNeedGeneration)
@@ -475,15 +492,7 @@ namespace MediaBrowser.Controller.Entities
if (recursive)
{
- // TODO: this is sometimes being called after the refresh has completed.
- try
- {
- ProviderManager.OnRefreshProgress(folder, percent);
- }
- catch (InvalidOperationException e)
- {
- Logger.LogError(e, "Error refreshing folder");
- }
+ ProviderManager.OnRefreshProgress(folder, percent);
}
});
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index bace703ad..44a1a85e3 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -138,7 +138,7 @@ namespace MediaBrowser.Controller.Library
MediaProtocol GetPathProtocol(string path);
- void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
+ void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user);
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index ec34a868b..93073cc79 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
@@ -19,7 +17,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions);
+ List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from artist.
@@ -28,7 +26,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions);
+ List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from genre.
@@ -37,6 +35,6 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions);
+ List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
index 699c15f93..52581df45 100644
--- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
@@ -54,7 +54,7 @@ namespace MediaBrowser.Controller.LiveTv
public string ChannelGroup { get; set; }
/// <summary>
- /// Gets or sets the the image path if it can be accessed directly from the file system.
+ /// Gets or sets the image path if it can be accessed directly from the file system.
/// </summary>
/// <value>The image path.</value>
public string ImagePath { get; set; }
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b6738e7cc..eb375c8a2 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -1,6 +1,8 @@
#nullable disable
#pragma warning disable CS1591
+// We need lowercase normalized string for ffmpeg
+#pragma warning disable CA1308
using System;
using System.Collections.Generic;
@@ -26,6 +28,14 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public partial class EncodingHelper
{
+ /// <summary>
+ /// The codec validation regex.
+ /// This regular expression matches strings that consist of alphanumeric characters, hyphens,
+ /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
+ /// This should matches all common valid codecs.
+ /// </summary>
+ public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
+
private const string QsvAlias = "qs";
private const string VaapiAlias = "va";
private const string D3d11vaAlias = "dx11";
@@ -51,6 +61,9 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
+ private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
+
+ private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -94,7 +107,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "wmav2", 2 },
{ "libmp3lame", 2 },
{ "libfdk_aac", 6 },
- { "aac_at", 6 },
{ "ac3", 6 },
{ "eac3", 6 },
{ "dca", 6 },
@@ -253,6 +265,15 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync);
}
+ private bool IsVideoToolboxFullSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("videotoolbox")
+ && _mediaEncoder.SupportsFilter("yadif_videotoolbox")
+ && _mediaEncoder.SupportsFilter("overlay_videotoolbox")
+ && _mediaEncoder.SupportsFilter("tonemap_videotoolbox")
+ && _mediaEncoder.SupportsFilter("scale_vt");
+ }
+
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
@@ -272,12 +293,15 @@ namespace MediaBrowser.Controller.MediaEncoding
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isD3d11vaDecoder = vidDecoder.Contains("d3d11va", StringComparison.OrdinalIgnoreCase);
- return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
+ var isVideoToolBoxDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
}
return state.VideoStream.VideoRange == VideoRange.HDR
&& (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
- || state.VideoStream.VideoRangeType == VideoRangeType.HLG);
+ || state.VideoStream.VideoRangeType == VideoRangeType.HLG
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -305,7 +329,23 @@ namespace MediaBrowser.Controller.MediaEncoding
// Native VPP tonemapping may come to QSV in the future.
return state.VideoStream.VideoRange == VideoRange.HDR
- && state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
+ && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
+ }
+
+ private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
+ {
+ if (state.VideoStream is null
+ || !options.EnableVideoToolboxTonemapping
+ || GetVideoColorBitDepth(state) != 10)
+ {
+ return false;
+ }
+
+ // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
+ // All other HDR formats working.
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
}
/// <summary>
@@ -362,7 +402,10 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libtheora";
}
- return codec.ToLowerInvariant();
+ if (_validationRegex.IsMatch(codec))
+ {
+ return codec.ToLowerInvariant();
+ }
}
return "copy";
@@ -400,7 +443,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
- if (string.IsNullOrEmpty(container))
+ if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
{
return null;
}
@@ -656,6 +699,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
+ if (!_validationRegex.IsMatch(codec))
+ {
+ codec = "aac";
+ }
+
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
{
// Use Apple's aac encoder if available as it provides best audio quality
@@ -703,6 +751,15 @@ namespace MediaBrowser.Controller.MediaEncoding
return "dca";
}
+ if (string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
+ {
+ // The ffmpeg upstream breaks the AudioToolbox ALAC encoder in version 6.1 but fixes it in version 7.0.
+ // Since ALAC is lossless in quality and the AudioToolbox encoder is not faster,
+ // its only benefit is a smaller file size.
+ // To prevent problems, use the ffmpeg native encoder instead.
+ return "alac";
+ }
+
return codec.ToLowerInvariant();
}
@@ -1071,7 +1128,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- // no videotoolbox hw filter.
+ // videotoolbox hw filter does not require device selection
args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias));
}
else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase))
@@ -1197,7 +1254,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Disable auto inserted SW scaler for HW decoders in case of changed resolution.
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
- if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4))
+ if (!isSwDecoder)
{
arg.Append(" -noautoscale");
}
@@ -1214,23 +1271,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1
- || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("264", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("avc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsH265(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1
- || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("265", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAAC(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
- return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
+ return codec.Contains("aac", StringComparison.OrdinalIgnoreCase);
}
public static string GetBitStreamArgs(MediaStream stream)
@@ -1284,7 +1341,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return ".ts";
}
- public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
+ private string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
if (state.OutputVideoBitrate is null)
{
@@ -1348,6 +1405,14 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
+ if (string.Equals(videoCodec, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase))
+ {
+ // The `maxrate` and `bufsize` options can potentially lead to performance regression
+ // and even encoder hangs, especially when the value is very high.
+ return FormattableString.Invariant($" -b:v {bitrate} -qmin -1 -qmax -1");
+ }
+
return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
@@ -1818,6 +1883,31 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -gops_per_idr 1";
}
}
+ else if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase) // h264 (h264_videotoolbox)
+ || string.Equals(videoEncoder, "hevc_videotoolbox", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_videotoolbox)
+ {
+ switch (encodingOptions.EncoderPreset)
+ {
+ case "veryslow":
+ case "slower":
+ case "slow":
+ case "medium":
+ param += " -prio_speed 0";
+ break;
+
+ case "fast":
+ case "faster":
+ case "veryfast":
+ case "superfast":
+ case "ultrafast":
+ param += " -prio_speed 1";
+ break;
+
+ default:
+ param += " -prio_speed 1";
+ break;
+ }
+ }
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
{
// Values 0-3, 0 being highest quality but slower
@@ -2181,7 +2271,16 @@ namespace MediaBrowser.Controller.MediaEncoding
return false;
}
- if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase))
+ // DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats
+
+ var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)
+ && !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
+ || (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
+ || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)))
{
return false;
}
@@ -4954,22 +5053,29 @@ namespace MediaBrowser.Controller.MediaEncoding
return (null, null, null);
}
- var swFilterChain = GetSwVidFilterChain(state, options, vidEncoder);
+ var isMacOS = OperatingSystem.IsMacOS();
+ var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var isVtFullSupported = isMacOS && IsVideoToolboxFullSupported();
- if (!options.EnableHardwareEncoding)
+ // legacy videotoolbox pipeline (disable hw filters)
+ if (!isVtEncoder
+ || !isVtFullSupported
+ || !_mediaEncoder.SupportsFilter("alphasrc"))
{
- return swFilterChain;
+ return GetSwVidFilterChain(state, options, vidEncoder);
}
- 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;
- }
+ // preferred videotoolbox + metal filters pipeline
+ return GetAppleVidFiltersPreferred(state, options, vidDecoder, vidEncoder);
+ }
- var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
- var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
- var doDeintH2645 = doDeintH264 || doDeintHevc;
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetAppleVidFiltersPreferred(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidDecoder,
+ string vidEncoder)
+ {
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
var reqW = state.BaseRequest.Width;
@@ -4977,33 +5083,121 @@ namespace MediaBrowser.Controller.MediaEncoding
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)
+ var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+
+ 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 doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
+ var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
+
+ var scaleFormat = string.Empty;
+ // Use P010 for Metal tone mapping, otherwise force an 8bit output.
+ if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase))
{
- return swFilterChain;
+ if (doMetalTonemap)
+ {
+ if (!string.Equals(state.VideoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase))
+ {
+ scaleFormat = "p010le";
+ }
+ }
+ else
+ {
+ scaleFormat = "nv12";
+ }
}
- // ffmpeg cannot use videotoolbox to scale
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
- newfilters.Add(swScaleFilter);
+ var hwScaleFilter = GetHwScaleFilter("vt", scaleFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
+ var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
+ var hasAssSubs = hasSubs
+ && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
+
+ if (!isVtEncoder)
+ {
+ // should not happen.
+ return (null, null, null);
+ }
- // 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");
+ /* Make main filters for video stream */
+ var mainFilters = new List<string>();
+ // hw deint
if (doDeintH2645)
{
var deintFilter = GetHwDeinterlaceFilter(state, options, "videotoolbox");
- newfilters.Add(deintFilter);
+ mainFilters.Add(deintFilter);
+ }
+
+ if (doVtTonemap)
+ {
+ const string VtTonemapArgs = "color_matrix=bt709:color_primaries=bt709:color_transfer=bt709";
+
+ // scale_vt can handle scaling & tonemapping in one shot, just like vpp_qsv.
+ hwScaleFilter = string.IsNullOrEmpty(hwScaleFilter)
+ ? "scale_vt=" + VtTonemapArgs
+ : hwScaleFilter + ":" + VtTonemapArgs;
+ }
+
+ // hw scale & vt tonemap
+ mainFilters.Add(hwScaleFilter);
+
+ // Metal tonemap
+ if (doMetalTonemap)
+ {
+ var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12");
+ mainFilters.Add(tonemapFilter);
+ }
+
+ /* Make sub and overlay filters for subtitle stream */
+ var subFilters = new List<string>();
+ var overlayFilters = new List<string>();
+
+ if (hasSubs)
+ {
+ if (hasGraphicalSubs)
+ {
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
+ subFilters.Add("format=bgra");
+ }
+ else if (hasTextSubs)
+ {
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
+ var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
+ subFilters.Add(alphaSrcFilter);
+ subFilters.Add("format=bgra");
+ subFilters.Add(subTextSubtitlesFilter);
+ }
+
+ subFilters.Add("hwupload=derive_device=videotoolbox");
+ overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
}
- return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
+ var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
+ subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
+ overlayFilters.Any(f => !string.IsNullOrEmpty(f));
+
+ // This is a workaround for ffmpeg's hwupload implementation
+ // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame
+ // will cause the encoder to produce incorrect frames.
+ if (needFiltering)
+ {
+ // INPUT videotoolbox/memory surface(vram/uma)
+ // this will pass-through automatically if in/out format matches.
+ mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
+ mainFilters.Insert(0, "hwupload=derive_device=videotoolbox");
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
}
/// <summary>
@@ -5995,22 +6189,22 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ // VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
+ // For example: https://trac.ffmpeg.org/ticket/10884
+ // Disable it for now.
+ const bool UseHwSurface = false;
+
if (is8bitSwFormatsVt)
{
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "h264", bitDepth, false);
- }
-
- if (string.Equals("mpeg2video", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
- {
- return GetHwaccelType(state, options, "mpeg2video", bitDepth, false);
+ return GetHwaccelType(state, options, "h264", bitDepth, UseHwSurface);
}
- if (string.Equals("mpeg4", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "mpeg4", bitDepth, false);
+ return GetHwaccelType(state, options, "vp8", bitDepth, UseHwSurface);
}
}
@@ -6019,12 +6213,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "hevc", bitDepth, false);
+ return GetHwaccelType(state, options, "hevc", bitDepth, UseHwSurface);
}
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp9", bitDepth, false);
+ return GetHwaccelType(state, options, "vp9", bitDepth, UseHwSurface);
}
}
@@ -6265,6 +6459,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
inputModifier += " -re";
}
+ else if (encodingOptions.EnableSegmentDeletion
+ && state.VideoStream is not null
+ && state.TranscodingType == TranscodingJobType.Hls
+ && IsCopyCodec(state.OutputVideoCodec)
+ && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateOption)
+ {
+ // Set an input read rate limit 10x for using SegmentDeletion with stream-copy
+ // to prevent ffmpeg from exiting prematurely (due to fast drive)
+ inputModifier += " -readrate 10";
+ }
var flags = new List<string>();
if (state.IgnoreInputDts)
@@ -6464,7 +6668,7 @@ namespace MediaBrowser.Controller.MediaEncoding
while (shiftAudioCodecs.Contains(audioCodecs[0], StringComparison.OrdinalIgnoreCase))
{
- var removed = shiftAudioCodecs[0];
+ var removed = audioCodecs[0];
audioCodecs.RemoveAt(0);
audioCodecs.Add(removed);
}
@@ -6498,7 +6702,7 @@ namespace MediaBrowser.Controller.MediaEncoding
while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparison.OrdinalIgnoreCase))
{
- var removed = shiftVideoCodecs[0];
+ var removed = videoCodecs[0];
videoCodecs.RemoveAt(0);
videoCodecs.Add(removed);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index c2cef4978..e696fa52c 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -149,6 +149,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="maxWidth">The maximum width.</param>
/// <param name="interval">The interval.</param>
/// <param name="allowHwAccel">Allow for hardware acceleration.</param>
+ /// <param name="enableHwEncoding">Use hardware mjpeg encoder.</param>
/// <param name="threads">The input/output thread count for ffmpeg.</param>
/// <param name="qualityScale">The qscale value for ffmpeg.</param>
/// <param name="priority">The process priority for the ffmpeg process.</param>
@@ -163,6 +164,7 @@ namespace MediaBrowser.Controller.MediaEncoding
int maxWidth,
TimeSpan interval,
bool allowHwAccel,
+ bool enableHwEncoding,
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
index 1e6d5933c..2b6540ea8 100644
--- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
@@ -137,6 +137,11 @@ public sealed class TranscodingJob : IDisposable
public TranscodingThrottler? TranscodingThrottler { get; set; }
/// <summary>
+ /// Gets or sets transcoding segment cleaner.
+ /// </summary>
+ public TranscodingSegmentCleaner? TranscodingSegmentCleaner { get; set; }
+
+ /// <summary>
/// Gets or sets last ping date.
/// </summary>
public DateTime LastPingDate { get; set; }
@@ -239,6 +244,7 @@ public sealed class TranscodingJob : IDisposable
{
#pragma warning disable CA1849 // Can't await in lock block
TranscodingThrottler?.Stop().GetAwaiter().GetResult();
+ TranscodingSegmentCleaner?.Stop();
var process = Process;
@@ -276,5 +282,7 @@ public sealed class TranscodingJob : IDisposable
CancellationTokenSource = null;
TranscodingThrottler?.Dispose();
TranscodingThrottler = null;
+ TranscodingSegmentCleaner?.Dispose();
+ TranscodingSegmentCleaner = null;
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
new file mode 100644
index 000000000..67bfcb02f
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.MediaEncoding;
+
+/// <summary>
+/// Transcoding segment cleaner.
+/// </summary>
+public class TranscodingSegmentCleaner : IDisposable
+{
+ private readonly TranscodingJob _job;
+ private readonly ILogger<TranscodingSegmentCleaner> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IMediaEncoder _mediaEncoder;
+ private Timer? _timer;
+ private int _segmentLength;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingSegmentCleaner"/> class.
+ /// </summary>
+ /// <param name="job">Transcoding job dto.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TranscodingSegmentCleaner}"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="segmentLength">The segment length of this transcoding job.</param>
+ public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength)
+ {
+ _job = job;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _segmentLength = segmentLength;
+ }
+
+ /// <summary>
+ /// Start timer.
+ /// </summary>
+ public void Start()
+ {
+ _timer = new Timer(TimerCallback, null, 20000, 20000);
+ }
+
+ /// <summary>
+ /// Stop cleaner.
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Dispose cleaner.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose cleaner.
+ /// </summary>
+ /// <param name="disposing">Disposing.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ DisposeTimer();
+ }
+ }
+
+ private EncodingOptions GetOptions()
+ {
+ return _config.GetEncodingOptions();
+ }
+
+ private async void TimerCallback(object? state)
+ {
+ if (_job.HasExited)
+ {
+ DisposeTimer();
+ return;
+ }
+
+ var options = GetOptions();
+ var enableSegmentDeletion = options.EnableSegmentDeletion;
+ var segmentKeepSeconds = Math.Max(options.SegmentKeepSeconds, 20);
+
+ if (enableSegmentDeletion)
+ {
+ var downloadPositionTicks = _job.DownloadPositionTicks ?? 0;
+ var downloadPositionSeconds = Convert.ToInt64(TimeSpan.FromTicks(downloadPositionTicks).TotalSeconds);
+
+ if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds)
+ {
+ var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength;
+
+ if (idxMaxToDelete > 0)
+ {
+ await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ private async Task DeleteSegmentFiles(TranscodingJob job, long idxMin, long idxMax, int delayMs)
+ {
+ var path = job.Path ?? throw new ArgumentException("Path can't be null.");
+
+ _logger.LogDebug("Deleting segment file(s) index {Min} to {Max} from {Path}", idxMin, idxMax, path);
+
+ await Task.Delay(delayMs).ConfigureAwait(false);
+
+ try
+ {
+ if (job.Type == TranscodingJobType.Hls)
+ {
+ DeleteHlsSegmentFiles(path, idxMin, idxMax);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Error deleting segment file(s) {Path}", path);
+ }
+ }
+
+ private void DeleteHlsSegmentFiles(string outputFilePath, long idxMin, long idxMax)
+ {
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
+ var name = Path.GetFileNameWithoutExtension(outputFilePath);
+
+ var filesToDelete = _fileSystem.GetFilePaths(directory)
+ .Where(f => long.TryParse(Path.GetFileNameWithoutExtension(f).Replace(name, string.Empty, StringComparison.Ordinal), out var idx)
+ && (idx >= idxMin && idx <= idxMax));
+
+ List<Exception>? exs = null;
+ foreach (var file in filesToDelete)
+ {
+ try
+ {
+ _logger.LogDebug("Deleting HLS segment file {0}", file);
+ _fileSystem.DeleteFile(file);
+ }
+ catch (IOException ex)
+ {
+ (exs ??= new List<Exception>()).Add(ex);
+ _logger.LogDebug(ex, "Error deleting HLS segment file {Path}", file);
+ }
+ }
+
+ if (exs is not null)
+ {
+ throw new AggregateException("Error deleting HLS segment files", exs);
+ }
+ }
+
+ private void DisposeTimer()
+ {
+ if (_timer is not null)
+ {
+ _timer.Dispose();
+ _timer = null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs
index 813f13eae..b95e6ed51 100644
--- a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs
@@ -115,7 +115,7 @@ public class TranscodingThrottler : IDisposable
var options = GetOptions();
- if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
+ if (options.EnableThrottling && IsThrottleAllowed(_job, Math.Max(options.ThrottleDelaySeconds, 60)))
{
await PauseTranscoding().ConfigureAwait(false);
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 0a706c307..06386f2b8 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
+using System.Threading.Channels;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Model.Session;
@@ -21,26 +22,38 @@ namespace MediaBrowser.Controller.Net
/// </summary>
/// <typeparam name="TReturnDataType">The type of the T return data type.</typeparam>
/// <typeparam name="TStateType">The type of the T state type.</typeparam>
- public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable
+ public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IAsyncDisposable
where TStateType : WebSocketListenerState, new()
where TReturnDataType : class
{
+ private readonly Channel<bool> _channel = Channel.CreateUnbounded<bool>(new UnboundedChannelOptions
+ {
+ AllowSynchronousContinuations = false,
+ SingleReader = true,
+ SingleWriter = false
+ });
+
+ private readonly object _activeConnectionsLock = new();
+
/// <summary>
/// The _active connections.
/// </summary>
- private readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>> _activeConnections =
- new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
+ private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new();
/// <summary>
/// The logger.
/// </summary>
protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
+ private readonly Task _messageConsumerTask;
+
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
{
ArgumentNullException.ThrowIfNull(logger);
Logger = logger;
+
+ _messageConsumerTask = HandleMessages();
}
/// <summary>
@@ -113,75 +126,93 @@ namespace MediaBrowser.Controller.Net
InitialDelayMs = dueTimeMs
};
- lock (_activeConnections)
+ lock (_activeConnectionsLock)
{
- _activeConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>(message.Connection, cancellationTokenSource, state));
+ _activeConnections.Add((message.Connection, cancellationTokenSource, state));
}
}
- protected async Task SendData(bool force)
+ protected void SendData(bool force)
{
- Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>[] tuples;
+ _channel.Writer.TryWrite(force);
+ }
- lock (_activeConnections)
+ private async Task HandleMessages()
+ {
+ while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false))
{
- tuples = _activeConnections
- .Where(c =>
+ while (_channel.Reader.TryRead(out var force))
+ {
+ try
{
- if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested)
- {
- var state = c.Item3;
+ (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)[] tuples;
- if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs)
+ var now = DateTime.UtcNow;
+ lock (_activeConnectionsLock)
+ {
+ if (_activeConnections.Count == 0)
{
- return true;
+ continue;
}
+
+ tuples = _activeConnections
+ .Where(c =>
+ {
+ if (c.Connection.State != WebSocketState.Open || c.CancellationTokenSource.IsCancellationRequested)
+ {
+ return false;
+ }
+
+ var state = c.State;
+ return force || (now - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs;
+ })
+ .ToArray();
}
- return false;
- })
- .ToArray();
- }
+ if (tuples.Length == 0)
+ {
+ continue;
+ }
- IEnumerable<Task> GetTasks()
- {
- foreach (var tuple in tuples)
- {
- yield return SendData(tuple);
+ var data = await GetDataToSend().ConfigureAwait(false);
+ if (data is null)
+ {
+ continue;
+ }
+
+ IEnumerable<Task> GetTasks()
+ {
+ foreach (var tuple in tuples)
+ {
+ yield return SendDataInternal(data, tuple);
+ }
+ }
+
+ await Task.WhenAll(GetTasks()).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Failed to send updates to websockets");
+ }
}
}
-
- await Task.WhenAll(GetTasks()).ConfigureAwait(false);
}
- private async Task SendData(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> tuple)
+ private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple)
{
- var connection = tuple.Item1;
-
try
{
- var state = tuple.Item3;
+ var (connection, cts, state) = tuple;
+ var cancellationToken = cts.Token;
+ await connection.SendAsync(
+ new OutboundWebSocketMessage<TReturnDataType> { MessageType = Type, Data = data },
+ cancellationToken).ConfigureAwait(false);
- var cancellationToken = tuple.Item2.Token;
-
- var data = await GetDataToSend().ConfigureAwait(false);
-
- if (data is not null)
- {
- await connection.SendAsync(
- new OutboundWebSocketMessage<TReturnDataType>
- {
- MessageType = Type,
- Data = data
- },
- cancellationToken).ConfigureAwait(false);
-
- state.DateLastSendUtc = DateTime.UtcNow;
- }
+ state.DateLastSendUtc = DateTime.UtcNow;
}
catch (OperationCanceledException)
{
- if (tuple.Item2.IsCancellationRequested)
+ if (tuple.CancellationTokenSource.IsCancellationRequested)
{
DisposeConnection(tuple);
}
@@ -199,11 +230,11 @@ namespace MediaBrowser.Controller.Net
/// <param name="message">The message.</param>
private void Stop(WebSocketMessageInfo message)
{
- lock (_activeConnections)
+ lock (_activeConnectionsLock)
{
- var connection = _activeConnections.FirstOrDefault(c => c.Item1 == message.Connection);
+ var connection = _activeConnections.FirstOrDefault(c => c.Connection == message.Connection);
- if (connection is not null)
+ if (connection != default)
{
DisposeConnection(connection);
}
@@ -214,17 +245,17 @@ namespace MediaBrowser.Controller.Net
/// Disposes the connection.
/// </summary>
/// <param name="connection">The connection.</param>
- private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
+ private void DisposeConnection((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) connection)
{
- Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+ Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Connection.RemoteEndPoint, GetType().Name);
// TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
// connection.Item1.Dispose();
try
{
- connection.Item2.Cancel();
- connection.Item2.Dispose();
+ connection.CancellationTokenSource.Cancel();
+ connection.CancellationTokenSource.Dispose();
}
catch (ObjectDisposedException ex)
{
@@ -237,36 +268,37 @@ namespace MediaBrowser.Controller.Net
Logger.LogError(ex, "Error disposing websocket");
}
- lock (_activeConnections)
+ lock (_activeConnectionsLock)
{
_activeConnections.Remove(connection);
}
}
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
+ protected virtual async ValueTask DisposeAsyncCore()
{
- if (dispose)
+ try
{
- lock (_activeConnections)
+ _channel.Writer.TryComplete();
+ await _messageConsumerTask.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Disposing the message consumer failed");
+ }
+
+ lock (_activeConnectionsLock)
+ {
+ foreach (var connection in _activeConnections.ToArray())
{
- foreach (var connection in _activeConnections.ToArray())
- {
- DisposeConnection(connection);
- }
+ DisposeConnection(connection);
}
}
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- public void Dispose()
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
{
- Dispose(true);
+ await DisposeAsyncCore().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index bb68a3b6d..cbe4bd87f 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists;
namespace MediaBrowser.Controller.Playlists
@@ -11,6 +12,28 @@ namespace MediaBrowser.Controller.Playlists
public interface IPlaylistManager
{
/// <summary>
+ /// Gets the playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Playlist.</returns>
+ Playlist GetPlaylistForUser(Guid playlistId, Guid userId);
+
+ /// <summary>
+ /// Creates the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistCreationRequest"/>.</param>
+ /// <returns>The created playlist.</returns>
+ Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest request);
+
+ /// <summary>
+ /// Updates a playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task UpdatePlaylist(PlaylistUpdateRequest request);
+
+ /// <summary>
/// Gets the playlists.
/// </summary>
/// <param name="userId">The user identifier.</param>
@@ -18,11 +41,20 @@ namespace MediaBrowser.Controller.Playlists
IEnumerable<Playlist> GetPlaylists(Guid userId);
/// <summary>
- /// Creates the playlist.
+ /// Adds a share to the playlist.
+ /// </summary>
+ /// <param name="request">The <see cref="PlaylistUserUpdateRequest"/>.</param>
+ /// <returns>Task.</returns>
+ Task AddUserToShares(PlaylistUserUpdateRequest request);
+
+ /// <summary>
+ /// Removes a share from the playlist.
/// </summary>
- /// <param name="options">The options.</param>
- /// <returns>Task&lt;Playlist&gt;.</returns>
- Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options);
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="share">The share.</param>
+ /// <returns>Task.</returns>
+ Task RemoveUserFromShares(Guid playlistId, Guid userId, PlaylistUserPermissions share);
/// <summary>
/// Adds to playlist.
@@ -31,7 +63,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns>
- Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
+ Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary>
/// Removes from playlist.
@@ -39,7 +71,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryIds">The entry ids.</param>
/// <returns>Task.</returns>
- Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
+ Task RemoveItemFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds);
/// <summary>
/// Gets the playlists folder.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index ca032e7f6..34b34e578 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -16,24 +16,23 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
{
public class Playlist : Folder, IHasShares
{
- public static readonly IReadOnlyList<string> SupportedExtensions = new[]
- {
+ public static readonly IReadOnlyList<string> SupportedExtensions =
+ [
".m3u",
".m3u8",
".pls",
".wpl",
".zpl"
- };
+ ];
public Playlist()
{
- Shares = Array.Empty<Share>();
+ Shares = [];
OpenAccess = false;
}
@@ -41,7 +40,7 @@ namespace MediaBrowser.Controller.Playlists
public bool OpenAccess { get; set; }
- public Share[] Shares { get; set; }
+ public IReadOnlyList<PlaylistUserPermissions> Shares { get; set; }
[JsonIgnore]
public bool IsFile => IsPlaylistFile(Path);
@@ -130,7 +129,7 @@ namespace MediaBrowser.Controller.Playlists
protected override List<BaseItem> LoadChildren()
{
// Save a trip to the database
- return new List<BaseItem>();
+ return [];
}
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
@@ -145,7 +144,7 @@ namespace MediaBrowser.Controller.Playlists
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
- return new List<BaseItem>();
+ return [];
}
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
@@ -167,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
return base.GetChildren(user, true, query);
}
- public static List<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
{
if (user is not null)
{
@@ -192,9 +191,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { BaseItemKind.Audio },
- GenreIds = new[] { musicGenre.Id },
- OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Audio],
+ GenreIds = [musicGenre.Id],
+ OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)],
DtoOptions = options
});
}
@@ -204,9 +203,9 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { BaseItemKind.Audio },
- ArtistIds = new[] { musicArtist.Id },
- OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
+ IncludeItemTypes = [BaseItemKind.Audio],
+ ArtistIds = [musicArtist.Id],
+ OrderBy = [(ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending)],
DtoOptions = options
});
}
@@ -217,8 +216,8 @@ namespace MediaBrowser.Controller.Playlists
{
Recursive = true,
IsFolder = false,
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
- MediaTypes = new[] { mediaType },
+ OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
+ MediaTypes = [mediaType],
EnableTotalRecordCount = false,
DtoOptions = options
};
@@ -226,7 +225,7 @@ namespace MediaBrowser.Controller.Playlists
return folder.GetItemList(query);
}
- return new[] { item };
+ return [item];
}
public override bool IsVisible(User user)
@@ -248,12 +247,17 @@ namespace MediaBrowser.Controller.Playlists
}
var shares = Shares;
- if (shares.Length == 0)
+ if (shares.Count == 0)
{
return false;
}
- return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId));
+ return shares.Any(s => s.UserId.Equals(userId));
+ }
+
+ public override bool CanDelete(User user)
+ {
+ return user.HasPermission(PermissionKind.IsAdministrator) || user.Id.Equals(OwnerUserId);
}
public override bool IsVisibleStandalone(User user)
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index d4de97651..7fe2f64af 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -78,5 +78,10 @@ namespace MediaBrowser.Controller.Providers
return filePaths;
}
+
+ public bool IsAccessible(string path)
+ {
+ return _fileSystem.GetFileSystemEntryPaths(path).Any();
+ }
}
}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index 48d627691..6d7550ab5 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -16,5 +16,7 @@ namespace MediaBrowser.Controller.Providers
IReadOnlyList<string> GetFilePaths(string path);
IReadOnlyList<string> GetFilePaths(string path, bool clearCache, bool sort = false);
+
+ bool IsAccessible(string path);
}
}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 3a12a56f1..76d5d3a3f 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -134,6 +134,7 @@ namespace MediaBrowser.Controller.Session
/// <value>The now playing item.</value>
public BaseItemDto NowPlayingItem { get; set; }
+ [JsonIgnore]
public BaseItem FullNowPlayingItem { get; set; }
public BaseItemDto NowViewingItem { get; set; }
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index 894aebed4..9aa9c3548 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -32,6 +32,7 @@ namespace MediaBrowser.LocalMetadata.Images
"folder",
"poster",
"cover",
+ "jacket",
"default"
};
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index 8a870e0d9..a7e027d94 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -519,7 +519,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
private void FetchFromSharesNode(XmlReader reader, IHasShares item)
{
- var list = new List<Share>();
+ var list = new List<PlaylistUserPermissions>();
reader.MoveToContent();
reader.Read();
@@ -565,7 +565,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
}
- item.Shares = list.ToArray();
+ item.Shares = [.. list];
}
/// <summary>
@@ -830,12 +830,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
/// </summary>
/// <param name="reader">The xml reader.</param>
/// <returns>The share.</returns>
- protected Share? GetShare(XmlReader reader)
+ protected PlaylistUserPermissions? GetShare(XmlReader reader)
{
- var item = new Share();
-
reader.MoveToContent();
reader.Read();
+ string? userId = null;
+ var canEdit = false;
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -845,10 +845,10 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "UserId":
- item.UserId = reader.ReadNormalizedString();
+ userId = reader.ReadNormalizedString();
break;
case "CanEdit":
- item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+ canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
break;
default:
reader.Skip();
@@ -862,9 +862,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
// This is valid
- if (!string.IsNullOrWhiteSpace(item.UserId))
+ if (!string.IsNullOrWhiteSpace(userId) && Guid.TryParse(userId, out var guid))
{
- return item;
+ return new PlaylistUserPermissions(guid, canEdit);
}
return null;
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 5a7193079..ee0d10bea 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -420,19 +420,16 @@ namespace MediaBrowser.LocalMetadata.Savers
foreach (var share in item.Shares)
{
- if (share.UserId is not null)
- {
- await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
+ await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
- await writer.WriteElementStringAsync(null, "UserId", null, share.UserId).ConfigureAwait(false);
- await writer.WriteElementStringAsync(
- null,
- "CanEdit",
- null,
- share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
+ await writer.WriteElementStringAsync(null, "UserId", null, share.UserId.ToString()).ConfigureAwait(false);
+ await writer.WriteElementStringAsync(
+ null,
+ "CanEdit",
+ null,
+ share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
- await writer.WriteEndElementAsync().ConfigureAwait(false);
- }
+ await writer.WriteEndElementAsync().ConfigureAwait(false);
}
await writer.WriteEndElementAsync().ConfigureAwait(false);
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index a97cca6b8..a11440ced 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -139,7 +139,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
var processArgs = string.Format(
CultureInfo.InvariantCulture,
- "-dump_attachment:t \"\" -y -i {0} -t 0 -f null null",
+ "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
+ inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
inputPath);
int exitCode;
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index fdca28390..5f0779dc7 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -69,6 +69,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
"aac_at",
"libfdk_aac",
"ac3",
+ "alac",
"dca",
"libmp3lame",
"libopus",
@@ -128,6 +129,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
"overlay_vulkan",
// videotoolbox
"yadif_videotoolbox",
+ "scale_vt",
+ "overlay_videotoolbox",
+ "tonemap_videotoolbox",
// rkrga
"scale_rkrga",
"vpp_rkrga",
@@ -144,17 +148,18 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } }
};
- // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
+ // These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
+ // Refers to the versions in https://ffmpeg.org/download.html
private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
{
- { "libavutil", new Version(56, 14) },
- { "libavcodec", new Version(58, 18) },
- { "libavformat", new Version(58, 12) },
- { "libavdevice", new Version(58, 3) },
- { "libavfilter", new Version(7, 16) },
- { "libswscale", new Version(5, 1) },
- { "libswresample", new Version(3, 1) },
- { "libpostproc", new Version(55, 1) }
+ { "libavutil", new Version(56, 70) },
+ { "libavcodec", new Version(58, 134) },
+ { "libavformat", new Version(58, 76) },
+ { "libavdevice", new Version(58, 13) },
+ { "libavfilter", new Version(7, 110) },
+ { "libswscale", new Version(5, 9) },
+ { "libswresample", new Version(3, 9) },
+ { "libpostproc", new Version(55, 9) }
};
private readonly ILogger _logger;
@@ -174,7 +179,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions
- public static Version MinVersion { get; } = new Version(4, 0);
+ public static Version MinVersion { get; } = new Version(4, 4);
public static Version? MaxVersion { get; } = null;
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index cc6971c1b..807678025 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -154,12 +154,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
public void SetFFmpegPath()
{
- // 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
- var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
+ // 1) Check if the --ffmpeg CLI switch has been given
+ var ffmpegPath = _startupOptionFFmpegPath;
if (string.IsNullOrEmpty(ffmpegPath))
{
- // 2) Check if the --ffmpeg CLI switch has been given
- ffmpegPath = _startupOptionFFmpegPath;
+ // 2) Custom path stored in config/encoding xml file under tag <EncoderAppPath> should be used as a fallback
+ ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
if (string.IsNullOrEmpty(ffmpegPath))
{
// 3) Check "ffmpeg"
@@ -463,6 +463,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
extraArgs += " -user_agent " + userAgent;
}
+ if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
+ {
+ extraArgs += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
+ }
+
return extraArgs;
}
@@ -691,7 +696,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
Video3DFormat.HalfTopAndBottom => @"crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
// ftab crop height in half, set the display aspect,crop out any black bars we may have made
Video3DFormat.FullTopAndBottom => @"crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,setsar=sar=1",
- _ => "scale=trunc(iw*sar):ih"
+ _ => "scale=round(iw*sar/2)*2:round(ih/2)*2"
};
filters.Add(scaler);
@@ -800,6 +805,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
int maxWidth,
TimeSpan interval,
bool allowHwAccel,
+ bool enableHwEncoding,
int? threads,
int? qualityScale,
ProcessPriorityClass? priority,
@@ -828,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
MediaPath = inputFile,
OutputVideoCodec = "mjpeg"
};
- var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
+ var vidEncoder = enableHwEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec;
// Get input and filter arguments
var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim();
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 317aba418..5397a6752 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -79,6 +79,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"5/8erl in Ehr'n",
"Smith/Kotzen",
"We;Na",
+ "LSR/CITY",
};
/// <summary>
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 8bace15c6..a07a0f41b 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -321,7 +321,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
catch (IOException ex)
{
- (exs ??= new List<Exception>(4)).Add(ex);
+ (exs ??= new List<Exception>()).Add(ex);
_logger.LogError(ex, "Error deleting HLS file {Path}", file);
}
}
@@ -546,6 +546,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
if (!transcodingJob.HasExited)
{
StartThrottler(state, transcodingJob);
+ StartSegmentCleaner(state, transcodingJob);
}
else if (transcodingJob.ExitCode != 0)
{
@@ -573,6 +574,22 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
&& state.IsInputVideo
&& state.VideoType == VideoType.VideoFile;
+ private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
+ {
+ if (EnableSegmentCleaning(state))
+ {
+ transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.CreateLogger<TranscodingSegmentCleaner>(), _serverConfigurationManager, _fileSystem, _mediaEncoder, state.SegmentLength);
+ transcodingJob.TranscodingSegmentCleaner.Start();
+ }
+ }
+
+ private static bool EnableSegmentCleaning(StreamState state)
+ => state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
+ && state.IsInputVideo
+ && state.TranscodingType == TranscodingJobType.Hls
+ && state.RunTimeTicks.HasValue
+ && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
+
private TranscodingJob OnTranscodeBeginning(
string path,
string? playSessionId,
@@ -724,7 +741,14 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
foreach (var file in _fileSystem.GetFilePaths(path, true))
{
- _fileSystem.DeleteFile(file);
+ try
+ {
+ _fileSystem.DeleteFile(file);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
+ }
}
}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 84c735f9c..9a192f584 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -28,6 +28,7 @@ public class EncodingOptions
VaapiDevice = "/dev/dri/renderD128";
EnableTonemapping = false;
EnableVppTonemapping = false;
+ EnableVideoToolboxTonemapping = false;
TonemappingAlgorithm = "bt2390";
TonemappingMode = "auto";
TonemappingRange = "auto";
@@ -50,7 +51,6 @@ public class EncodingOptions
EnableHardwareEncoding = true;
AllowHevcEncoding = false;
AllowAv1Encoding = false;
- AllowMjpegEncoding = false;
EnableSubtitleExtraction = true;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
@@ -147,6 +147,11 @@ public class EncodingOptions
public bool EnableVppTonemapping { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether videotoolbox tonemapping is enabled.
+ /// </summary>
+ public bool EnableVideoToolboxTonemapping { get; set; }
+
+ /// <summary>
/// Gets or sets the tone-mapping algorithm.
/// </summary>
public string TonemappingAlgorithm { get; set; }
@@ -257,11 +262,6 @@ public class EncodingOptions
public bool AllowAv1Encoding { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether MJPEG encoding is enabled.
- /// </summary>
- public bool AllowMjpegEncoding { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether subtitle extraction is enabled.
/// </summary>
public bool EnableSubtitleExtraction { get; set; }
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 42148a276..e777d5fd8 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -27,6 +27,8 @@ namespace MediaBrowser.Model.Configuration
SeasonZeroDisplayName = "Specials";
}
+ public bool Enabled { get; set; } = true;
+
public bool EnablePhotos { get; set; }
public bool EnableRealtimeMonitor { get; set; }
diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
index 92c16ee84..a151d3429 100644
--- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs
+++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs
@@ -14,6 +14,11 @@ public class TrickplayOptions
public bool EnableHwAcceleration { get; set; } = false;
/// <summary>
+ /// Gets or sets a value indicating whether or not to use HW accelerated MJPEG encoding.
+ /// </summary>
+ public bool EnableHwEncoding { get; set; } = false;
+
+ /// <summary>
/// Gets or sets the behavior used by trickplay provider on library scan/update.
/// </summary>
public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 7d9449b74..55d1c3d51 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -345,7 +345,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
/// <param name="type">The <see cref="DlnaProfileType"/>.</param>
/// <param name="playProfile">The <see cref="DirectPlayProfile"/> object to get the video stream from.</param>
- /// <returns>The the normalized input container.</returns>
+ /// <returns>The normalized input container.</returns>
public static string? NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, DeviceProfile? profile, DlnaProfileType type, DirectPlayProfile? playProfile = null)
{
if (string.IsNullOrEmpty(inputContainer))
@@ -557,7 +557,7 @@ namespace MediaBrowser.Model.Dlna
private static void SetStreamInfoOptionsFromDirectPlayProfile(MediaOptions options, MediaSourceInfo item, StreamInfo playlistItem, DirectPlayProfile? directPlayProfile)
{
var container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
- var protocol = MediaStreamProtocol.Http;
+ var protocol = MediaStreamProtocol.http;
item.TranscodingContainer = container;
item.TranscodingSubProtocol = protocol;
@@ -648,7 +648,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == PlayMethod.DirectPlay)
{
- playlistItem.SubProtocol = MediaStreamProtocol.Http;
+ playlistItem.SubProtocol = MediaStreamProtocol.http;
var audioStreamIndex = directPlayInfo.AudioStreamIndex ?? audioStream?.Index;
if (audioStreamIndex.HasValue)
@@ -803,7 +803,7 @@ namespace MediaBrowser.Model.Dlna
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
// Enforce HLS video codec restrictions
- if (playlistItem.SubProtocol == MediaStreamProtocol.Hls)
+ if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
{
videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
}
@@ -840,7 +840,7 @@ namespace MediaBrowser.Model.Dlna
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
// Enforce HLS audio codec restrictions
- if (playlistItem.SubProtocol == MediaStreamProtocol.Hls)
+ if (playlistItem.SubProtocol == MediaStreamProtocol.hls)
{
if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
@@ -1350,7 +1350,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="transcoderSupport">The <see cref="ITranscoderSupport"/>.</param>
/// <param name="outputContainer">The output container.</param>
/// <param name="transcodingSubProtocol">The subtitle transoding protocol.</param>
- /// <returns>The the normalized input container.</returns>
+ /// <returns>The normalized input container.</returns>
public static SubtitleProfile GetSubtitleProfile(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
@@ -1360,7 +1360,7 @@ namespace MediaBrowser.Model.Dlna
string? outputContainer,
MediaStreamProtocol? transcodingSubProtocol)
{
- if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.Hls))
+ if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls))
{
// Look for supported embedded subs of the same format
foreach (var profile in subtitleProfiles)
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index cd6d34be2..75e5b6d18 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -670,7 +670,7 @@ namespace MediaBrowser.Model.Dlna
if (MediaType == DlnaProfileType.Audio)
{
- if (SubProtocol == MediaStreamProtocol.Hls)
+ if (SubProtocol == MediaStreamProtocol.hls)
{
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
@@ -678,7 +678,7 @@ namespace MediaBrowser.Model.Dlna
return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}
- if (SubProtocol == MediaStreamProtocol.Hls)
+ if (SubProtocol == MediaStreamProtocol.hls)
{
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
@@ -716,7 +716,7 @@ namespace MediaBrowser.Model.Dlna
long startPositionTicks = item.StartPositionTicks;
- if (item.SubProtocol == MediaStreamProtocol.Hls)
+ if (item.SubProtocol == MediaStreamProtocol.hls)
{
list.Add(new NameValuePair("StartTimeTicks", string.Empty));
}
@@ -778,7 +778,7 @@ namespace MediaBrowser.Model.Dlna
list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
- if (item.SubProtocol == MediaStreamProtocol.Hls)
+ if (item.SubProtocol == MediaStreamProtocol.hls)
{
list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
@@ -829,7 +829,7 @@ namespace MediaBrowser.Model.Dlna
var list = new List<SubtitleStreamInfo>();
// HLS will preserve timestamps so we can just grab the full subtitle stream
- long startPositionTicks = SubProtocol == MediaStreamProtocol.Hls
+ long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
? 0
: (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 8f4f3e2f8..891448c66 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -27,7 +27,7 @@ namespace MediaBrowser.Model.Dlna
public string AudioCodec { get; set; } = string.Empty;
[XmlAttribute("protocol")]
- public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.Http;
+ public MediaStreamProtocol Protocol { get; set; } = MediaStreamProtocol.http;
[DefaultValue(false)]
[XmlAttribute("estimateContentLength")]
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index cfff717db..6d5c84e1d 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -65,7 +65,7 @@ namespace MediaBrowser.Model.Dto
public DateTime? DateLastMediaAdded { get; set; }
- public string ExtraType { get; set; }
+ public ExtraType? ExtraType { get; set; }
public int? AirsBeforeSeasonNumber { get; set; }
diff --git a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs
index e1894d84a..fc4cfdd66 100644
--- a/MediaBrowser.Model/Entities/CollectionTypeOptions.cs
+++ b/MediaBrowser.Model/Entities/CollectionTypeOptions.cs
@@ -1,16 +1,49 @@
-#pragma warning disable CS1591
+#pragma warning disable SA1300 // Lowercase required for backwards compat.
-namespace MediaBrowser.Model.Entities
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// The collection type options.
+/// </summary>
+public enum CollectionTypeOptions
{
- public enum CollectionTypeOptions
- {
- Movies = 0,
- TvShows = 1,
- Music = 2,
- MusicVideos = 3,
- HomeVideos = 4,
- BoxSets = 5,
- Books = 6,
- Mixed = 7
- }
+ /// <summary>
+ /// Movies.
+ /// </summary>
+ movies = 0,
+
+ /// <summary>
+ /// TV Shows.
+ /// </summary>
+ tvshows = 1,
+
+ /// <summary>
+ /// Music.
+ /// </summary>
+ music = 2,
+
+ /// <summary>
+ /// Music Videos.
+ /// </summary>
+ musicvideos = 3,
+
+ /// <summary>
+ /// Home Videos (and Photos).
+ /// </summary>
+ homevideos = 4,
+
+ /// <summary>
+ /// Box Sets.
+ /// </summary>
+ boxsets = 5,
+
+ /// <summary>
+ /// Books.
+ /// </summary>
+ books = 6,
+
+ /// <summary>
+ /// Mixed Movies and TV Shows.
+ /// </summary>
+ mixed = 7
}
diff --git a/MediaBrowser.Model/Entities/IHasShares.cs b/MediaBrowser.Model/Entities/IHasShares.cs
index b34d1a037..8c4ba6c42 100644
--- a/MediaBrowser.Model/Entities/IHasShares.cs
+++ b/MediaBrowser.Model/Entities/IHasShares.cs
@@ -1,4 +1,6 @@
-namespace MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Entities;
/// <summary>
/// Interface for access to shares.
@@ -8,5 +10,5 @@ public interface IHasShares
/// <summary>
/// Gets or sets the shares.
/// </summary>
- Share[] Shares { get; set; }
+ IReadOnlyList<PlaylistUserPermissions> Shares { get; set; }
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index ae4a008bb..0d2d7c696 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -707,34 +707,48 @@ namespace MediaBrowser.Model.Entities
return (VideoRange.Unknown, VideoRangeType.Unknown);
}
- var colorTransfer = ColorTransfer;
-
- if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
- {
- return (VideoRange.HDR, VideoRangeType.HDR10);
- }
-
- if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
- {
- return (VideoRange.HDR, VideoRangeType.HLG);
- }
-
var codecTag = CodecTag;
var dvProfile = DvProfile;
var rpuPresentFlag = RpuPresentFlag == 1;
var blPresentFlag = BlPresentFlag == 1;
var dvBlCompatId = DvBlSignalCompatibilityId;
- var isDoViHDRProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
- var isDoViHDRFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4);
+ var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8;
+ var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6);
- if ((isDoViHDRProfile && isDoViHDRFlag)
+ if ((isDoViProfile && isDoViFlag)
|| string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
{
- return (VideoRange.HDR, VideoRangeType.DOVI);
+ return dvProfile switch
+ {
+ 5 => (VideoRange.HDR, VideoRangeType.DOVI),
+ 8 => dvBlCompatId switch
+ {
+ 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
+ 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
+ 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
+ // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist.
+ 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
+ // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
+ _ => (VideoRange.SDR, VideoRangeType.SDR)
+ },
+ 7 => (VideoRange.HDR, VideoRangeType.HDR10),
+ _ => (VideoRange.SDR, VideoRangeType.SDR)
+ };
+ }
+
+ var colorTransfer = ColorTransfer;
+
+ if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
+ {
+ return (VideoRange.HDR, VideoRangeType.HDR10);
+ }
+ else if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+ {
+ return (VideoRange.HDR, VideoRangeType.HLG);
}
return (VideoRange.SDR, VideoRangeType.SDR);
diff --git a/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs b/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs
new file mode 100644
index 000000000..b5f017d2b
--- /dev/null
+++ b/MediaBrowser.Model/Entities/PlaylistUserPermissions.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Class to hold data on user permissions for playlists.
+/// </summary>
+public class PlaylistUserPermissions
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaylistUserPermissions"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="canEdit">Edit permission.</param>
+ public PlaylistUserPermissions(Guid userId, bool canEdit = false)
+ {
+ UserId = userId;
+ CanEdit = canEdit;
+ }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user has edit permissions.
+ /// </summary>
+ public bool CanEdit { get; set; }
+}
diff --git a/MediaBrowser.Model/Entities/Share.cs b/MediaBrowser.Model/Entities/Share.cs
deleted file mode 100644
index 186aad189..000000000
--- a/MediaBrowser.Model/Entities/Share.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace MediaBrowser.Model.Entities;
-
-/// <summary>
-/// Class to hold data on sharing permissions.
-/// </summary>
-public class Share
-{
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- public string? UserId { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether the user has edit permissions.
- /// </summary>
- public bool CanEdit { get; set; }
-}
diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
index 2b2bda12c..89bb72c3c 100644
--- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
+++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
@@ -37,7 +37,6 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets the type of the collection.
/// </summary>
/// <value>The type of the collection.</value>
- [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
public CollectionTypeOptions? CollectionType { get; set; }
public LibraryOptions LibraryOptions { get; set; }
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
index 62d496d04..ec54b1afd 100644
--- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -18,7 +18,7 @@ public class PlaylistCreationRequest
/// <summary>
/// Gets or sets the list of items.
/// </summary>
- public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
+ public IReadOnlyList<Guid> ItemIdList { get; set; } = [];
/// <summary>
/// Gets or sets the media type.
@@ -31,7 +31,12 @@ public class PlaylistCreationRequest
public Guid UserId { get; set; }
/// <summary>
- /// Gets or sets the shares.
+ /// Gets or sets the user permissions.
/// </summary>
- public Share[]? Shares { get; set; }
+ public IReadOnlyList<PlaylistUserPermissions> Users { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool? Public { get; set; } = true;
}
diff --git a/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs
new file mode 100644
index 000000000..db290bbdb
--- /dev/null
+++ b/MediaBrowser.Model/Playlists/PlaylistUpdateRequest.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Playlists;
+
+/// <summary>
+/// A playlist update request.
+/// </summary>
+public class PlaylistUpdateRequest
+{
+ /// <summary>
+ /// Gets or sets the id of the playlist.
+ /// </summary>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the user updating the playlist.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the playlist.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets item ids to add to the playlist.
+ /// </summary>
+ public IReadOnlyList<Guid>? Ids { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist users.
+ /// </summary>
+ public IReadOnlyList<PlaylistUserPermissions>? Users { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playlist is public.
+ /// </summary>
+ public bool? Public { get; set; }
+}
diff --git a/MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs b/MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs
new file mode 100644
index 000000000..1840efdf3
--- /dev/null
+++ b/MediaBrowser.Model/Playlists/PlaylistUserUpdateRequest.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MediaBrowser.Model.Playlists;
+
+/// <summary>
+/// A playlist user update request.
+/// </summary>
+public class PlaylistUserUpdateRequest
+{
+ /// <summary>
+ /// Gets or sets the id of the playlist.
+ /// </summary>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the updated user.
+ /// </summary>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user can edit the playlist.
+ /// </summary>
+ public bool? CanEdit { get; set; }
+}
diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs
index b91fd8657..8126b8bfc 100644
--- a/MediaBrowser.Model/Search/SearchQuery.cs
+++ b/MediaBrowser.Model/Search/SearchQuery.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -31,7 +30,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the search term.
/// </summary>
/// <value>The search term.</value>
- public string SearchTerm { get; set; }
+ public required string SearchTerm { get; set; }
/// <summary>
/// Gets or sets the start index. Used for paging.
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index 5f51fb21c..fc1f24ae1 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -35,11 +35,11 @@ namespace MediaBrowser.Model.Session
// TODO: Remove after 10.9
[Obsolete("Unused")]
[DefaultValue(false)]
- public bool? SupportsContentUploading { get; set; }
+ public bool? SupportsContentUploading { get; set; } = false;
// TODO: Remove after 10.9
[Obsolete("Unused")]
[DefaultValue(false)]
- public bool? SupportsSync { get; set; }
+ public bool? SupportsSync { get; set; } = false;
}
}
diff --git a/MediaBrowser.Model/System/LogFile.cs b/MediaBrowser.Model/System/LogFile.cs
index aec910c92..d4eb6bafc 100644
--- a/MediaBrowser.Model/System/LogFile.cs
+++ b/MediaBrowser.Model/System/LogFile.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -29,6 +28,6 @@ namespace MediaBrowser.Model.System
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
- public string Name { get; set; }
+ public required string Name { get; set; }
}
}
diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
index 0536f4ef7..bc8438855 100644
--- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Model.Tasks
/// <param name="lastResult">Result of the last run triggered task.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="taskName">The name of the task.</param>
- /// <param name="isApplicationStartup">Whether or not this is is fired during startup.</param>
+ /// <param name="isApplicationStartup">Whether or not this is fired during startup.</param>
void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup);
/// <summary>
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 06445c90d..6f473fc07 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -773,7 +773,8 @@ namespace MediaBrowser.Providers.Manager
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
}
- MergeData(temp, metadata, item.LockedFields, true, false);
+ // Will always replace all metadata when Scan for new and updated files is used. Else, follow the options.
+ MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false);
}
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index f34034964..0b1fed0a3 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -968,16 +968,13 @@ namespace MediaBrowser.Providers.Manager
var id = item.Id;
_logger.LogDebug("OnRefreshProgress {Id:N} {Progress}", id, progress);
- // TODO: Need to hunt down the conditions for this happening
- _activeRefreshes.AddOrUpdate(
- id,
- _ => throw new InvalidOperationException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running",
- item.GetType().Name,
- item.Id.ToString("N", CultureInfo.InvariantCulture))),
- (_, _) => progress);
+ if (!_activeRefreshes.TryGetValue(id, out var current)
+ || progress <= current
+ || !_activeRefreshes.TryUpdate(id, progress, current))
+ {
+ // Item isn't currently refreshing, or update was received out-of-order, so don't trigger event.
+ return;
+ }
try
{
@@ -1106,7 +1103,8 @@ namespace MediaBrowser.Providers.Manager
var musicArtists = albums
.Select(i => i.MusicArtist)
- .Where(i => i is not null);
+ .Where(i => i is not null)
+ .Distinct();
var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, cancellationToken));
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 4d4b59b8c..67b84681d 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -228,6 +228,7 @@ namespace MediaBrowser.Providers.MediaInfo
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
audio.Size = mediaInfo.Size;
+ audio.PremiereDate = mediaInfo.PremiereDate;
if (!audio.IsLocked)
{
@@ -348,9 +349,9 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (!audio.LockedFields.Contains(MetadataField.Name))
+ if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
{
- audio.Name = options.ReplaceAllMetadata || string.IsNullOrEmpty(audio.Name) ? tags.Title : audio.Name;
+ audio.Name = tags.Title;
}
if (options.ReplaceAllMetadata)
@@ -370,7 +371,11 @@ namespace MediaBrowser.Providers.MediaInfo
{
var year = Convert.ToInt32(tags.Year);
audio.ProductionYear = year;
- audio.PremiereDate = new DateTime(year, 01, 01);
+
+ if (!audio.PremiereDate.HasValue)
+ {
+ audio.PremiereDate = new DateTime(year, 01, 01);
+ }
}
if (!audio.LockedFields.Contains(MetadataField.Genres))
@@ -402,7 +407,14 @@ namespace MediaBrowser.Providers.MediaInfo
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
{
- audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId);
+ // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
+ // See https://github.com/mono/taglib-sharp/issues/304
+ var mediaInfo = await GetMediaInfo(audio, CancellationToken.None).ConfigureAwait(false);
+ var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
+ if (trackMbId is not null)
+ {
+ audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
+ }
}
// Save extracted lyrics if they exist,
@@ -426,5 +438,20 @@ namespace MediaBrowser.Providers.MediaInfo
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
currentStreams.AddRange(externalLyricFiles);
}
+
+ private async Task<Model.MediaInfo.MediaInfo> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
+ {
+ var request = new MediaInfoRequest
+ {
+ MediaType = DlnaProfileType.Audio,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = item.Path,
+ Protocol = item.PathProtocol ?? MediaProtocol.File
+ }
+ };
+
+ return await _mediaEncoder.GetMediaInfo(request, cancellationToken).ConfigureAwait(false);
+ }
}
}
diff --git a/build b/build
deleted file mode 120000
index c07a74de4..000000000
--- a/build
+++ /dev/null
@@ -1 +0,0 @@
-build.sh \ No newline at end of file
diff --git a/build.sh b/build.sh
deleted file mode 100755
index 1db02af98..000000000
--- a/build.sh
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/env bash
-
-# build.sh - Build Jellyfin binary packages
-# Part of the Jellyfin Project
-
-set -o errexit
-set -o pipefail
-
-usage() {
- echo -e "build.sh - Build Jellyfin binary packages"
- echo -e "Usage:"
- echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
- echo -e "Notes:"
- echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
- echo -e " * native: Build using the build script in the host OS"
- echo -e " * docker: Build using the build script in a standardized Docker container"
- echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
- echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
- echo -e " retained after the build is finished; the source directory will still be cleaned"
- echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
- echo -e " the list of supported platforms and exit"
-}
-
-list_platforms() {
- declare -a platforms
- platforms=(
- $( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
- )
- echo -e "Valid platforms:"
- echo
- for platform in ${platforms[@]}; do
- echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
- done
-}
-
-do_build_native() {
- if [[ ! -f $( which dpkg ) || $( dpkg --print-architecture | head -1 ) != "${PLATFORM##*.}" ]]; then
- echo "Cross-building is not supported for native builds, use 'docker' builds on amd64 for cross-building."
- exit 1
- fi
- export IS_DOCKER=NO
- deployment/build.${PLATFORM}
-}
-
-do_build_docker() {
- if [[ -f $( which dpkg ) && $( dpkg --print-architecture | head -1 ) != "amd64" ]]; then
- echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
- exit 1
- fi
- if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
- echo "Missing Dockerfile for platform ${PLATFORM}"
- exit 1
- fi
- if [[ ${KEEP_ARTIFACTS} == YES ]]; then
- docker_args=""
- else
- docker_args="--rm"
- fi
-
- docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
- mkdir -p ${ARTIFACT_DIR}
- docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
-}
-
-while [[ $# -gt 0 ]]; do
- key="$1"
- case $key in
- -t|--type)
- BUILD_TYPE="$2"
- shift # past argument
- shift # past value
- ;;
- -p|--platform)
- PLATFORM="$2"
- shift # past argument
- shift # past value
- ;;
- -k|--keep-artifacts)
- KEEP_ARTIFACTS=YES
- shift # past argument
- ;;
- -l|--list-platforms)
- list_platforms
- exit 0
- ;;
- -h|--help)
- usage
- exit 0
- ;;
- *) # unknown option
- echo "Unknown option $1"
- usage
- exit 1
- ;;
- esac
-done
-
-if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
- usage
- exit 1
-fi
-
-export SOURCE_DIR="$( pwd )"
-export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
-
-# Determine build type
-case ${BUILD_TYPE} in
- native)
- do_build_native
- ;;
- docker)
- do_build_docker
- ;;
-esac
diff --git a/build.yaml b/build.yaml
deleted file mode 100644
index 464caf328..000000000
--- a/build.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-# We just wrap `build` so this is really it
-name: "jellyfin"
-version: "10.9.0"
-packages:
- - debian.amd64
- - debian.arm64
- - debian.armhf
- - ubuntu.amd64
- - ubuntu.arm64
- - ubuntu.armhf
- - fedora.amd64
- - centos.amd64
- - linux.amd64
- - windows.amd64
- - macos.amd64
- - macos.arm64
- - portable
diff --git a/bump_version b/bump_version
index dd55e62c7..72bbbfbf5 100755
--- a/bump_version
+++ b/bump_version
@@ -7,7 +7,7 @@ set -o pipefail
set -o xtrace
usage() {
- echo -e "bump_version - increase the shared version and generate changelogs"
+ echo -e "bump_version - increase the shared version"
echo -e ""
echo -e "Usage:"
echo -e " $ bump_version <new_version>"
@@ -19,7 +19,6 @@ if [[ -z $1 ]]; then
fi
shared_version_file="./SharedVersion.cs"
-build_file="./build.yaml"
# csproj files for nuget packages
jellyfin_subprojects=(
MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -31,29 +30,16 @@ jellyfin_subprojects=(
)
new_version="$1"
+new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
-# Parse the version from the AssemblyVersion
old_version="$(
grep "AssemblyVersion" ${shared_version_file} \
| sed -E 's/\[assembly: ?AssemblyVersion\("([0-9\.]+)"\)\]/\1/'
)"
-echo $old_version
-
-# Set the shared version to the specified new_version
-old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
-new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
-sed -i "s/${old_version_sed}/${new_version_sed}/g" ${shared_version_file}
+echo old assembly version: $old_version
-old_version="$(
- grep "version:" ${build_file} \
- | sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
-)"
-echo $old_version
-
-# Set the build.yaml version to the specified new_version
-old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
-new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
-sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
+# Set the assembly version to the specified new_version
+sed -i "s|${old_version}|${new_version_sed}|g" ${shared_version_file}
# update nuget package version
for subproject in ${jellyfin_subprojects[@]}; do
@@ -65,65 +51,11 @@ for subproject in ${jellyfin_subprojects[@]}; do
| sed -E 's/<VersionPrefix>([0-9\.]+[-a-z0-9]*)<\/VersionPrefix>/\1/'
)"
echo old nuget version: $old_version
- new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
# Set the nuget version to the specified new_version
sed -i "s|${old_version}|${new_version_sed}|g" ${subproject}
done
-if [[ ${new_version} == *"-"* ]]; then
- new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
- new_version_deb_sup=""
-else
- new_version_pkg="${new_version}"
- new_version_deb_sup="-1"
-fi
-
-# Update the metapackage equivs file
-debian_equivs_file="debian/metapackage/jellyfin"
-sed -i "s/${old_version_sed}/${new_version_pkg}/g" ${debian_equivs_file}
-
-# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
-debian_changelog_file="debian/changelog"
-debian_changelog_temp="$( mktemp )"
-# Create new temp file with our changelog
-echo -e "jellyfin-server (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
-
- * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-" >> ${debian_changelog_temp}
-cat ${debian_changelog_file} >> ${debian_changelog_temp}
-# Move into place
-mv ${debian_changelog_temp} ${debian_changelog_file}
-
-# Write out a temporary Dnf changelog with our new stuff prepended and some templated formatting
-fedora_spec_file="fedora/jellyfin.spec"
-fedora_changelog_temp="$( mktemp )"
-fedora_spec_temp_dir="$( mktemp -d )"
-fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin.spec.tmp"
-# Make a copy of our spec file for hacking
-cp ${fedora_spec_file} ${fedora_spec_temp_dir}/
-pushd ${fedora_spec_temp_dir}
-# Split out the stuff before and after changelog
-csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
-# Update the version in xx00
-sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
-# Remove the header from xx01
-sed -i '/^%changelog/d' xx01
-# Create new temp file with our changelog
-echo -e "%changelog
-* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
-cat xx01 >> ${fedora_changelog_temp}
-# Reassembble
-cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
-popd
-# Move into place
-mv ${fedora_spec_temp} ${fedora_spec_file}
-# Clean up
-rm -rf ${fedora_spec_temp_dir}
-
# Stage the changed files for commit
git add .
git status -v
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index 0d744c02a..000000000
--- a/debian/changelog
+++ /dev/null
@@ -1,89 +0,0 @@
-jellyfin-server (10.9.0-1) unstable; urgency=medium
-
- * New upstream version 10.9.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.9.0
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 13 Jul 2022 20:58:08 -0600
-
-jellyfin-server (10.8.0-1) unstable; urgency=medium
-
- * Forthcoming stable release
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:55:12 -0500
-
-jellyfin-server (10.7.0-1) unstable; urgency=medium
-
- * Forthcoming stable release
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:09:45 -0400
-
-jellyfin-server (10.6.0-2) unstable; urgency=medium
-
- * Fix upgrade bug
-
- -- Joshua Boniface <joshua@boniface.me> Sun, 19 Jul 22:47:27 -0400
-
-jellyfin-server (10.6.0-1) unstable; urgency=medium
-
- * Forthcoming stable release
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 23 Mar 2020 14:46:05 -0400
-
-jellyfin (10.5.0-1) unstable; urgency=medium
-
- * New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 11 Oct 2019 20:12:38 -0400
-
-jellyfin (10.4.0-1) unstable; urgency=medium
-
- * New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 31 Aug 2019 21:38:56 -0400
-
-jellyfin (10.3.7-1) unstable; urgency=medium
-
- * New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 24 Jul 2019 10:48:28 -0400
-
-jellyfin (10.3.6-1) unstable; urgency=medium
-
- * New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 06 Jul 2019 13:34:19 -0400
-
-jellyfin (10.3.5-1) unstable; urgency=medium
-
- * New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 09 Jun 2019 21:47:35 -0400
-
-jellyfin (10.3.4-1) unstable; urgency=medium
-
- * New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 06 Jun 2019 22:45:31 -0400
-
-jellyfin (10.3.3-1) unstable; urgency=medium
-
- * New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 17 May 2019 23:12:08 -0400
-
-jellyfin (10.3.2-1) unstable; urgency=medium
-
- * New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 30 Apr 2019 20:18:44 -0400
-
-jellyfin (10.3.1-1) unstable; urgency=medium
-
- * New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 20 Apr 2019 14:24:07 -0400
-
-jellyfin (10.3.0-1) unstable; urgency=medium
-
- * New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index 45a4fb75d..000000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-8
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
deleted file mode 100644
index af460fedc..000000000
--- a/debian/conf/jellyfin
+++ /dev/null
@@ -1,53 +0,0 @@
-# Jellyfin default configuration options
-# This is a POSIX shell fragment
-
-# Use this file to override the default configurations; add additional
-# options with JELLYFIN_ADD_OPTS.
-
-# Under systemd, use
-# /etc/systemd/system/jellyfin.service.d/jellyfin.service.conf
-# to override the user or this config file's location.
-
-#
-# General options
-#
-
-# Program directories
-JELLYFIN_DATA_DIR="/var/lib/jellyfin"
-JELLYFIN_CONFIG_DIR="/etc/jellyfin"
-JELLYFIN_LOG_DIR="/var/log/jellyfin"
-JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
-
-# web client path, installed by the jellyfin-web package
-JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
-
-# ffmpeg binary paths, overriding the system values
-JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
-
-# Disable glibc dynamic heap adjustment
-MALLOC_TRIM_THRESHOLD_=131072
-
-# [OPTIONAL] run Jellyfin as a headless service
-#JELLYFIN_SERVICE_OPT="--service"
-
-# [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
-
-# Space to add additional command line options to jellyfin (for help see ~$ jellyfin --help)
-JELLYFIN_ADDITIONAL_OPTS=""
-
-# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
-# 0 = Workstation
-# 1 = Server
-#COMPlus_gcServer=1
-
-#
-# SysV init/Upstart options
-#
-# Note: These options are ignored by systemd; use /etc/systemd/system/jellyfin.d overrides instead.
-#
-
-# Application username
-JELLYFIN_USER="jellyfin"
-# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS --datadir $JELLYFIN_DATA_DIR --configdir $JELLYFIN_CONFIG_DIR --logdir $JELLYFIN_LOG_DIR --cachedir $JELLYFIN_CACHE_DIR"
diff --git a/debian/conf/jellyfin.service.conf b/debian/conf/jellyfin.service.conf
deleted file mode 100644
index 1f92d7d94..000000000
--- a/debian/conf/jellyfin.service.conf
+++ /dev/null
@@ -1,55 +0,0 @@
-# Jellyfin systemd configuration options
-
-# Use this file to override the user or environment file location.
-
-[Service]
-# Alter the user that Jellyfin runs as
-#User = jellyfin
-
-# Alter where environment variables are sourced from
-#EnvironmentFile = /etc/default/jellyfin
-
-# Service hardening options
-# These were added in PR #6953 to solve issue #6952, but some combination of
-# them causes "restart.sh" functionality to break with the following error:
-# sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the
-# 'nosuid' option set or an NFS file system without root privileges?
-# See issue #7503 for details on the troubleshooting that went into this.
-# Since these were added for NixOS specifically and are above and beyond
-# what 99% of systemd units do, they have been moved here as optional
-# additional flags to set for maximum system security and can be enabled at
-# the administrator's or package maintainer's discretion.
-# Uncomment these only if you know what you're doing, and doing so may cause
-# bugs with in-server Restart and potentially other functionality as well.
-#NoNewPrivileges=true
-#SystemCallArchitectures=native
-#RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
-#RestrictNamespaces=false
-#RestrictRealtime=true
-#RestrictSUIDSGID=true
-#ProtectControlGroups=false
-#ProtectHostname=true
-#ProtectKernelLogs=false
-#ProtectKernelModules=false
-#ProtectKernelTunables=false
-#LockPersonality=true
-#PrivateTmp=false
-#PrivateDevices=false
-#PrivateUsers=true
-#RemoveIPC=true
-#SystemCallFilter=~@clock
-#SystemCallFilter=~@aio
-#SystemCallFilter=~@chown
-#SystemCallFilter=~@cpu-emulation
-#SystemCallFilter=~@debug
-#SystemCallFilter=~@keyring
-#SystemCallFilter=~@memlock
-#SystemCallFilter=~@module
-#SystemCallFilter=~@mount
-#SystemCallFilter=~@obsolete
-#SystemCallFilter=~@privileged
-#SystemCallFilter=~@raw-io
-#SystemCallFilter=~@reboot
-#SystemCallFilter=~@setuid
-#SystemCallFilter=~@swap
-#SystemCallErrorNumber=EPERM
diff --git a/debian/conf/logging.json b/debian/conf/logging.json
deleted file mode 100644
index f32b2089e..000000000
--- a/debian/conf/logging.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "Serilog": {
- "MinimumLevel": "Information",
- "WriteTo": [
- {
- "Name": "Console",
- "Args": {
- "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
- }
- },
- {
- "Name": "Async",
- "Args": {
- "configure": [
- {
- "Name": "File",
- "Args": {
- "path": "%JELLYFIN_LOG_DIR%//jellyfin.log",
- "fileSizeLimitBytes": 10485700,
- "rollOnFileSizeLimit": true,
- "retainedFileCountLimit": 10,
- "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message}{NewLine}{Exception}"
- }
- }
- ]
- }
- }
- ]
- }
-}
diff --git a/debian/control b/debian/control
deleted file mode 100644
index 5e0460de9..000000000
--- a/debian/control
+++ /dev/null
@@ -1,27 +0,0 @@
-Source: jellyfin-server
-Section: misc
-Priority: optional
-Maintainer: Jellyfin Team <team@jellyfin.org>
-Build-Depends: debhelper (>= 9),
- dotnet-sdk-8.0,
- libc6-dev,
- libcurl4-openssl-dev,
- libfontconfig1-dev,
- libfreetype6-dev,
- libssl-dev
-Standards-Version: 3.9.4
-Homepage: https://jellyfin.org/
-Vcs-Git: https://github.org/jellyfin/jellyfin.git
-Vcs-Browser: https://github.org/jellyfin/jellyfin
-
-Package: jellyfin-server
-Replaces: jellyfin (<<10.6.0)
-Breaks: jellyfin (<<10.6.0)
-Architecture: any
-Depends: libsqlite3-0,
- libfontconfig1,
- libfreetype6,
- libssl1.1 | libssl3
-Recommends: jellyfin-web
-Description: Jellyfin is the Free Software Media System.
- This package provides the Jellyfin server backend and API.
diff --git a/debian/copyright b/debian/copyright
deleted file mode 100644
index 0d7a2a600..000000000
--- a/debian/copyright
+++ /dev/null
@@ -1,29 +0,0 @@
-Format: http://dep.debian.net/deps/dep5
-Upstream-Name: jellyfin
-Source: https://github.com/jellyfin/jellyfin
-
-Files: *
-Copyright: 2018 Jellyfin Team
-License: GPL-2.0+
-
-Files: debian/*
-Copyright: 2018 Joshua Boniface <joshua@boniface.me>
-Copyright: 2014 Carlos Hernandez <carlos@techbyte.ca>
-License: GPL-2.0+
-
-License: GPL-2.0+
- This package is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
- .
- This package is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- .
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>
- .
- On Debian systems, the complete text of the GNU General
- Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
diff --git a/debian/gbp.conf b/debian/gbp.conf
deleted file mode 100644
index 60b3d2872..000000000
--- a/debian/gbp.conf
+++ /dev/null
@@ -1,6 +0,0 @@
-[DEFAULT]
-pristine-tar = False
-cleaner = fakeroot debian/rules clean
-
-[import-orig]
-filter = [ ".git*", ".hg*", ".vs*", ".vscode*" ]
diff --git a/debian/install b/debian/install
deleted file mode 100644
index 0b48dd7a2..000000000
--- a/debian/install
+++ /dev/null
@@ -1,4 +0,0 @@
-usr/lib/jellyfin usr/lib/
-debian/conf/jellyfin etc/default/
-debian/conf/logging.json etc/jellyfin/
-debian/conf/jellyfin.service.conf etc/systemd/system/jellyfin.service.d/
diff --git a/debian/jellyfin.init b/debian/jellyfin.init
deleted file mode 100644
index 784536d87..000000000
--- a/debian/jellyfin.init
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/bin/sh
-### BEGIN INIT INFO
-# Provides: Jellyfin Media Server
-# Required-Start: $local_fs $network
-# Required-Stop: $local_fs
-# Default-Start: 2 3 4 5
-# Default-Stop: 0 1 6
-# Short-Description: Jellyfin Media Server
-# Description: Runs Jellyfin Server
-### END INIT INFO
-
-set -e
-
-# Carry out specific functions when asked to by the system
-
-if test -f /etc/default/jellyfin; then
- . /etc/default/jellyfin
-fi
-
-. /lib/lsb/init-functions
-
-PIDFILE="/run/jellyfin.pid"
-
-case "$1" in
- start)
- log_daemon_msg "Starting Jellyfin Media Server" "jellyfin" || true
-
- if start-stop-daemon --start --quiet --oknodo --background --pidfile $PIDFILE --make-pidfile --user $JELLYFIN_USER --chuid $JELLYFIN_USER --exec /usr/bin/jellyfin -- $JELLYFIN_ARGS; then
- log_end_msg 0 || true
- else
- log_end_msg 1 || true
- fi
- ;;
-
- stop)
- log_daemon_msg "Stopping Jellyfin Media Server" "jellyfin" || true
- if start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE --remove-pidfile; then
- log_end_msg 0 || true
- else
- log_end_msg 1 || true
- fi
- ;;
-
- restart)
- log_daemon_msg "Restarting Jellyfin Media Server" "jellyfin" || true
- start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $PIDFILE --remove-pidfile
- if start-stop-daemon --start --quiet --oknodo --background --pidfile $PIDFILE --make-pidfile --user $JELLYFIN_USER --chuid $JELLYFIN_USER --exec /usr/bin/jellyfin -- $JELLYFIN_ARGS; then
- log_end_msg 0 || true
- else
- log_end_msg 1 || true
- fi
- ;;
-
- status)
- status_of_proc -p $PIDFILE /usr/bin/jellyfin jellyfin && exit 0 || exit $?
- ;;
-
- *)
- echo "Usage: $0 {start|stop|restart|status}"
- exit 1
- ;;
-esac
diff --git a/debian/jellyfin.service b/debian/jellyfin.service
deleted file mode 100644
index 2cc49f7c4..000000000
--- a/debian/jellyfin.service
+++ /dev/null
@@ -1,17 +0,0 @@
-[Unit]
-Description = Jellyfin Media Server
-After = network-online.target
-
-[Service]
-Type = simple
-EnvironmentFile = /etc/default/jellyfin
-User = jellyfin
-Group = jellyfin
-WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
-Restart = on-failure
-TimeoutSec = 15
-SuccessExitStatus=0 143
-
-[Install]
-WantedBy = multi-user.target
diff --git a/debian/jellyfin.upstart b/debian/jellyfin.upstart
deleted file mode 100644
index ef5bc9bca..000000000
--- a/debian/jellyfin.upstart
+++ /dev/null
@@ -1,20 +0,0 @@
-description "jellyfin daemon"
-
-start on (local-filesystems and net-device-up IFACE!=lo)
-stop on runlevel [!2345]
-
-console log
-respawn
-respawn limit 10 5
-
-kill timeout 20
-
-script
- set -x
- echo "Starting $UPSTART_JOB"
-
- # Log file
- logger -t "$0" "DEBUG: `set`"
- . /etc/default/jellyfin
- exec su -u $JELLYFIN_USER -c /usr/bin/jellyfin $JELLYFIN_ARGS
-end script
diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin
deleted file mode 100644
index 8787c3a49..000000000
--- a/debian/metapackage/jellyfin
+++ /dev/null
@@ -1,13 +0,0 @@
-Source: jellyfin
-Section: misc
-Priority: optional
-Homepage: https://jellyfin.org
-Standards-Version: 3.9.2
-
-Package: jellyfin
-Version: 10.9.0
-Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
-Depends: jellyfin-server, jellyfin-web
-Description: Provides the Jellyfin Free Software Media System
- Provides the full Jellyfin experience, including both the server and web interface.
-
diff --git a/debian/po/POTFILES.in b/debian/po/POTFILES.in
deleted file mode 100644
index cef83a340..000000000
--- a/debian/po/POTFILES.in
+++ /dev/null
@@ -1 +0,0 @@
-[type: gettext/rfc822deb] templates
diff --git a/debian/po/templates.pot b/debian/po/templates.pot
deleted file mode 100644
index 2cdcae417..000000000
--- a/debian/po/templates.pot
+++ /dev/null
@@ -1,57 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: jellyfin-server\n"
-"Report-Msgid-Bugs-To: jellyfin-server@packages.debian.org\n"
-"POT-Creation-Date: 2015-06-12 20:51-0600\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=CHARSET\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#. Type: note
-#. Description
-#: ../templates:1001
-msgid "Jellyfin permission info:"
-msgstr ""
-
-#. Type: note
-#. Description
-#: ../templates:1001
-msgid ""
-"Jellyfin by default runs under a user named \"jellyfin\". Please ensure that the "
-"user jellyfin has read and write access to any folders you wish to add to your "
-"library. Otherwise please run jellyfin under a different user."
-msgstr ""
-
-#. Type: string
-#. Description
-#: ../templates:2001
-msgid "Username to run Jellyfin as:"
-msgstr ""
-
-#. Type: string
-#. Description
-#: ../templates:2001
-msgid "The user that jellyfin will run as."
-msgstr ""
-
-#. Type: note
-#. Description
-#: ../templates:3001
-msgid "Jellyfin still running"
-msgstr ""
-
-#. Type: note
-#. Description
-#: ../templates:3001
-msgid "Jellyfin is currently running. Please close it and try again."
-msgstr ""
diff --git a/debian/postinst b/debian/postinst
deleted file mode 100644
index 947959aa7..000000000
--- a/debian/postinst
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/bin/bash
-set -e
-
-NAME=jellyfin
-DEFAULT_FILE=/etc/default/${NAME}
-
-# Source Jellyfin default configuration
-if [[ -f $DEFAULT_FILE ]]; then
- . $DEFAULT_FILE
-fi
-
-JELLYFIN_USER=${JELLYFIN_USER:-jellyfin}
-RENDER_GROUP=${RENDER_GROUP:-render}
-VIDEO_GROUP=${VIDEO_GROUP:-video}
-
-# Data directories for program data (cache, db), configs, and logs
-PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
-CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
-LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
-CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
-
-case "$1" in
- configure)
- # create jellyfin group if it does not exist
- if [[ -z "$(getent group ${JELLYFIN_USER})" ]]; then
- addgroup --quiet --system ${JELLYFIN_USER} > /dev/null 2>&1
- fi
- # create jellyfin user if it does not exist
- if [[ -z "$(getent passwd ${JELLYFIN_USER})" ]]; then
- adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \
- --gecos "Jellyfin default user" > /dev/null 2>&1
- fi
- # add jellyfin to the render group for hwa
- if [[ ! -z "$(getent group ${RENDER_GROUP})" ]]; then
- usermod -aG ${RENDER_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
- fi
- # add jellyfin to the video group for hwa
- if [[ ! -z "$(getent group ${VIDEO_GROUP})" ]]; then
- usermod -aG ${VIDEO_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
- fi
- # ensure $PROGRAMDATA exists
- if [[ ! -d $PROGRAMDATA ]]; then
- mkdir $PROGRAMDATA
- fi
- # ensure $CONFIGDATA exists
- if [[ ! -d $CONFIGDATA ]]; then
- mkdir $CONFIGDATA
- fi
- # ensure $LOGDATA exists
- if [[ ! -d $LOGDATA ]]; then
- mkdir $LOGDATA
- fi
- # ensure $CACHEDATA exists
- if [[ ! -d $CACHEDATA ]]; then
- mkdir $CACHEDATA
- fi
- # Ensure permissions are correct on all config directories
- chown -R ${JELLYFIN_USER} $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
- chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
- chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
-
- # Install jellyfin symlink into /usr/bin
- ln -sf /usr/lib/jellyfin/bin/jellyfin /usr/bin/jellyfin
-
- ;;
- abort-upgrade|abort-remove|abort-deconfigure)
- ;;
- *)
- echo "postinst called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-
-#DEBHELPER
-
-if [[ -x "/usr/bin/deb-systemd-helper" ]]; then
- # Manual init script handling
- deb-systemd-helper unmask jellyfin.service >/dev/null || true
- # was-enabled defaults to true, so new installations run enable.
- if deb-systemd-helper --quiet was-enabled jellyfin.service; then
- # Enables the unit on first installation, creates new
- # symlinks on upgrades if the unit file has changed.
- deb-systemd-helper enable jellyfin.service >/dev/null || true
- else
- # Update the statefile to add new symlinks (if any), which need to be
- # cleaned up on purge. Also remove old symlinks.
- deb-systemd-helper update-state jellyfin.service >/dev/null || true
- fi
-fi
-
-# End automatically added section
-# Automatically added by dh_installinit
-if [[ "$1" == "configure" ]] || [[ "$1" == "abort-upgrade" ]]; then
- if [[ -d "/run/systemd/system" ]]; then
- systemctl --system daemon-reload >/dev/null || true
- deb-systemd-invoke start jellyfin >/dev/null || true
- elif [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
- update-rc.d jellyfin defaults >/dev/null
- invoke-rc.d jellyfin start || exit $?
- fi
-fi
-exit 0
diff --git a/debian/postrm b/debian/postrm
deleted file mode 100644
index 3d56a5f1e..000000000
--- a/debian/postrm
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/bin/bash
-set -e
-
-NAME=jellyfin
-DEFAULT_FILE=/etc/default/${NAME}
-
-# Source Jellyfin default configuration
-if [[ -f $DEFAULT_FILE ]]; then
- . $DEFAULT_FILE
-fi
-
-# Data directories for program data (cache, db), configs, and logs
-PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
-CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
-LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
-CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
-
-# In case this system is running systemd, we make systemd reload the unit files
-# to pick up changes.
-if [[ -d /run/systemd/system ]] ; then
- systemctl --system daemon-reload >/dev/null || true
-fi
-
-case "$1" in
- purge)
- echo PURGE | debconf-communicate $NAME > /dev/null 2>&1 || true
-
- if [[ -x "/etc/init.d/jellyfin" ]] || [[ -e "/etc/init/jellyfin.conf" ]]; then
- update-rc.d jellyfin remove >/dev/null 2>&1 || true
- fi
-
- if [[ -x "/usr/bin/deb-systemd-helper" ]]; then
- deb-systemd-helper purge jellyfin.service >/dev/null
- deb-systemd-helper unmask jellyfin.service >/dev/null
- fi
-
- # Remove user and group
- userdel jellyfin > /dev/null 2>&1 || true
- delgroup --quiet jellyfin > /dev/null 2>&1 || true
- # Remove config dir
- if [[ -d $CONFIGDATA ]]; then
- rm -rf $CONFIGDATA
- fi
- # Remove log dir
- if [[ -d $LOGDATA ]]; then
- rm -rf $LOGDATA
- fi
- # Remove cache dir
- if [[ -d $CACHEDATA ]]; then
- rm -rf $CACHEDATA
- fi
- # Remove program data dir
- if [[ -d $PROGRAMDATA ]]; then
- rm -rf $PROGRAMDATA
- fi
- # Remove binary symlink
- rm -f /usr/bin/jellyfin
- # Remove sudoers config
- [[ -f /etc/sudoers.d/jellyfin-sudoers ]] && rm /etc/sudoers.d/jellyfin-sudoers
- # Remove anything at the default locations; catches situations where the user moved the defaults
- [[ -e /etc/jellyfin ]] && rm -rf /etc/jellyfin
- [[ -e /var/log/jellyfin ]] && rm -rf /var/log/jellyfin
- [[ -e /var/cache/jellyfin ]] && rm -rf /var/cache/jellyfin
- [[ -e /var/lib/jellyfin ]] && rm -rf /var/lib/jellyfin
- ;;
- remove)
- if [[ -x "/usr/bin/deb-systemd-helper" ]]; then
- deb-systemd-helper mask jellyfin.service >/dev/null
- fi
- ;;
- upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
- ;;
- *)
- echo "postrm called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-
-#DEBHELPER#
-
-exit 0
diff --git a/debian/preinst b/debian/preinst
deleted file mode 100644
index 2713fb9b8..000000000
--- a/debian/preinst
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/bin/bash
-set -e
-
-NAME=jellyfin
-DEFAULT_FILE=/etc/default/${NAME}
-
-# Source Jellyfin default configuration
-if [[ -f $DEFAULT_FILE ]]; then
- . $DEFAULT_FILE
-fi
-
-# Data directories for program data (cache, db), configs, and logs
-PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
-CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
-LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
-CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
-
-# In case this system is running systemd, we make systemd reload the unit files
-# to pick up changes.
-if [[ -d /run/systemd/system ]] ; then
- systemctl --system daemon-reload >/dev/null || true
-fi
-
-case "$1" in
- install|upgrade)
- # try graceful termination;
- if [[ -d /run/systemd/system ]]; then
- deb-systemd-invoke stop ${NAME}.service > /dev/null 2>&1 || true
- elif [ -x "/etc/init.d/${NAME}" ] || [ -e "/etc/init/${NAME}.conf" ]; then
- invoke-rc.d ${NAME} stop > /dev/null 2>&1 || true
- fi
- # try and figure out if jellyfin is running
- PIDFILE=$(find /var/run/ -maxdepth 1 -mindepth 1 -name "jellyfin*.pid" -print -quit)
- [[ -n "$PIDFILE" ]] && [[ -s "$PIDFILE" ]] && JELLYFIN_PID=$(cat ${PIDFILE})
- # if its running, let's stop it
- if [[ -n "$JELLYFIN_PID" ]]; then
- echo "Stopping Jellyfin!"
- # if jellyfin is still running, kill it
- if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
- CPIDS=$(pgrep -P $JELLYFIN_PID)
- sleep 2 && kill -KILL $CPIDS
- kill -TERM $CPIDS > /dev/null 2>&1
- fi
- sleep 1
- # if it's still running, show error
- if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
- echo "Could not successfully stop JellyfinServer, please do so before uninstalling."
- exit 1
- else
- [[ -f $PIDFILE ]] && rm $PIDFILE
- fi
- fi
-
- # Clean up old Emby cruft that can break the user's system
- [[ -f /etc/sudoers.d/emby ]] && rm -f /etc/sudoers.d/emby
-
- # If we have existing config, log, or cache dirs in /var/lib/jellyfin, move them into the right place
- if [[ -d $PROGRAMDATA/config ]]; then
- mv $PROGRAMDATA/config $CONFIGDATA
- fi
- if [[ -d $PROGRAMDATA/logs ]]; then
- mv $PROGRAMDATA/logs $LOGDATA
- fi
- if [[ -d $PROGRAMDATA/logs ]]; then
- mv $PROGRAMDATA/cache $CACHEDATA
- fi
-
- ;;
- abort-upgrade)
- ;;
- *)
- echo "preinst called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-#DEBHELPER#
-
-exit 0
diff --git a/debian/prerm b/debian/prerm
deleted file mode 100644
index e965cb7d7..000000000
--- a/debian/prerm
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/bin/bash
-set -e
-
-NAME=jellyfin
-DEFAULT_FILE=/etc/default/${NAME}
-
-# Source Jellyfin default configuration
-if [[ -f $DEFAULT_FILE ]]; then
- . $DEFAULT_FILE
-fi
-
-# Data directories for program data (cache, db), configs, and logs
-PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
-CONFIGDATA=${JELLYFIN_CONFIG_DIRECTORY-/etc/$NAME}
-LOGDATA=${JELLYFIN_LOG_DIRECTORY-/var/log/$NAME}
-CACHEDATA=${JELLYFIN_CACHE_DIRECTORY-/var/cache/$NAME}
-
-case "$1" in
- remove|upgrade|deconfigure)
- echo "Stopping Jellyfin!"
- # try graceful termination;
- if [[ -d /run/systemd/system ]]; then
- deb-systemd-invoke stop ${NAME}.service > /dev/null 2>&1 || true
- elif [ -x "/etc/init.d/${NAME}" ] || [ -e "/etc/init/${NAME}.conf" ]; then
- invoke-rc.d ${NAME} stop > /dev/null 2>&1 || true
- fi
- # Ensure that it is shutdown
- PIDFILE=$(find /var/run/ -maxdepth 1 -mindepth 1 -name "jellyfin*.pid" -print -quit)
- [[ -n "$PIDFILE" ]] && [[ -s "$PIDFILE" ]] && JELLYFIN_PID=$(cat ${PIDFILE})
- # if its running, let's stop it
- if [[ -n "$JELLYFIN_PID" ]]; then
- # if jellyfin is still running, kill it
- if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
- CPIDS=$(pgrep -P $JELLYFIN_PID)
- sleep 2 && kill -KILL $CPIDS
- kill -TERM $CPIDS > /dev/null 2>&1
- fi
- sleep 1
- # if it's still running, show error
- if [[ -n "$(ps -p $JELLYFIN_PID -o pid=)" ]]; then
- echo "Could not successfully stop Jellyfin, please do so before uninstalling."
- exit 1
- else
- [[ -f $PIDFILE ]] && rm $PIDFILE
- fi
- fi
- if [[ -f /usr/lib/jellyfin/bin/MediaBrowser.Server.Mono.exe.so ]]; then
- rm /usr/lib/jellyfin/bin/MediaBrowser.Server.Mono.exe.so
- fi
- ;;
- failed-upgrade)
- ;;
- *)
- echo "prerm called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-
-#DEBHELPER#
-
-exit 0
diff --git a/debian/rules b/debian/rules
deleted file mode 100755
index 79cd55a15..000000000
--- a/debian/rules
+++ /dev/null
@@ -1,55 +0,0 @@
-#! /usr/bin/make -f
-CONFIG := Release
-TERM := xterm
-SHELL := /bin/bash
-
-HOST_ARCH := $(shell arch)
-BUILD_ARCH := ${DEB_HOST_MULTIARCH}
-ifeq ($(HOST_ARCH),x86_64)
- # Building AMD64
- DOTNETRUNTIME := linux-x64
- ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
- # Cross-building ARM on AMD64
- DOTNETRUNTIME := linux-arm
- endif
- ifeq ($(BUILD_ARCH),aarch64-linux-gnu)
- # Cross-building ARM on AMD64
- DOTNETRUNTIME := linux-arm64
- endif
-endif
-ifeq ($(HOST_ARCH),armv7l)
- # Building ARM
- DOTNETRUNTIME := linux-arm
-endif
-ifeq ($(HOST_ARCH),arm64)
- # Building ARM
- DOTNETRUNTIME := linux-arm64
-endif
-ifeq ($(HOST_ARCH),aarch64)
- # Building ARM
- DOTNETRUNTIME := linux-arm64
-endif
-
-export DH_VERBOSE=1
-export DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-%:
- dh $@
-
-# disable "make check"
-override_dh_auto_test:
-
-# disable stripping debugging symbols
-override_dh_clistrip:
-
-override_dh_auto_build:
- dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
- -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
-
-override_dh_auto_clean:
- dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
- rm -rf '$(CURDIR)/usr'
-
-# Force the service name to jellyfin even if we're building jellyfin-nightly
-override_dh_installinit:
- dh_installinit --name=jellyfin
diff --git a/debian/source.lintian-overrides b/debian/source.lintian-overrides
deleted file mode 100644
index aeb332f13..000000000
--- a/debian/source.lintian-overrides
+++ /dev/null
@@ -1,3 +0,0 @@
-# This is an override for the following lintian errors:
-jellyfin source: license-problem-md5sum-non-free-file Emby.Drawing/ImageMagick/fonts/webdings.ttf*
-jellyfin source: source-is-missing
diff --git a/debian/source/format b/debian/source/format
deleted file mode 100644
index d3827e75a..000000000
--- a/debian/source/format
+++ /dev/null
@@ -1 +0,0 @@
-1.0
diff --git a/debian/source/options b/debian/source/options
deleted file mode 100644
index 17b5373d5..000000000
--- a/debian/source/options
+++ /dev/null
@@ -1,11 +0,0 @@
-tar-ignore='.git*'
-tar-ignore='**/.git'
-tar-ignore='**/.hg'
-tar-ignore='**/.vs'
-tar-ignore='**/.vscode'
-tar-ignore='deployment'
-tar-ignore='**/bin'
-tar-ignore='**/obj'
-tar-ignore='**/.nuget'
-tar-ignore='*.deb'
-tar-ignore='ThirdParty'
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
deleted file mode 100644
index 6bd7d312c..000000000
--- a/deployment/Dockerfile.centos.amd64
+++ /dev/null
@@ -1,39 +0,0 @@
-FROM quay.io/centos/centos:stream9
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV IS_DOCKER=YES
-
-# Prepare CentOS environment
-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 \
- && dnf clean all \
- && rm -rf /var/cache/dnf
-
-# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/85bcc525-4e9c-471e-9c1d-96259aa1a315/930833ef34f66fe9ee2643b0ba21621a/dotnet-sdk-8.0.201-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
-
-# Create symlinks and directories
-RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \
- && mkdir -p ${SOURCE_DIR}/SPECS \
- && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
- && mkdir -p ${SOURCE_DIR}/SOURCES \
- && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
deleted file mode 100644
index da0c9dabd..000000000
--- a/deployment/Dockerfile.debian.amd64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yq \
- && apt-get install --no-install-recommends -yq \
- debhelper gnupg devscripts build-essential mmv \
- libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \
- libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yq \
- && apt-get autoremove -yq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.amd64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
deleted file mode 100644
index 6c4cb816f..000000000
--- a/deployment/Dockerfile.debian.arm64
+++ /dev/null
@@ -1,46 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts build-essential mmv
-
-# Prepare the cross-toolchain
-RUN dpkg --add-architecture arm64 \
- && apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 12 \
- && cd cross-gcc-packages-amd64/cross-gcc-12-arm64 \
- && apt-get install --no-install-recommends -yqq \
- gcc-12-source libstdc++-12-dev-arm64-cross \
- binutils-aarch64-linux-gnu bison flex libtool \
- gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
- systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip \
- libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 \
- libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 \
- libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust1:arm64 libstdc++-12-dev:arm64 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
deleted file mode 100644
index b1fa6cee5..000000000
--- a/deployment/Dockerfile.debian.armhf
+++ /dev/null
@@ -1,47 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts build-essential mmv
-
-# Prepare the cross-toolchain
-RUN dpkg --add-architecture armhf \
- && apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 12 \
- && cd cross-gcc-packages-amd64/cross-gcc-12-armhf \
- && apt-get install --no-install-recommends -yqq \
- gcc-12-source libstdc++-12-dev-armhf-cross \
- binutils-aarch64-linux-gnu bison flex libtool gdb \
- sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \
- systemtap-sdt-dev autogen expect chrpath zlib1g-dev \
- zip binutils-arm-linux-gnueabihf libc6-dev:armhf \
- linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \
- libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf \
- liblttng-ust1:armhf libstdc++-12-dev:armhf \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
deleted file mode 100644
index ca16a08fb..000000000
--- a/deployment/Dockerfile.docker.amd64
+++ /dev/null
@@ -1,12 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-ARG SOURCE_DIR=/src
-ARG ARTIFACT_DIR=/jellyfin
-
-WORKDIR ${SOURCE_DIR}
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-RUN dotnet publish Jellyfin.Server --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
deleted file mode 100644
index 6e0f7d18e..000000000
--- a/deployment/Dockerfile.docker.arm64
+++ /dev/null
@@ -1,12 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-ARG SOURCE_DIR=/src
-ARG ARTIFACT_DIR=/jellyfin
-
-WORKDIR ${SOURCE_DIR}
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-RUN dotnet publish Jellyfin.Server --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
deleted file mode 100644
index 44fb705e6..000000000
--- a/deployment/Dockerfile.docker.armhf
+++ /dev/null
@@ -1,12 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-ARG SOURCE_DIR=/src
-ARG ARTIFACT_DIR=/jellyfin
-
-WORKDIR ${SOURCE_DIR}
-COPY . .
-ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-
-RUN dotnet publish Jellyfin.Server --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
deleted file mode 100644
index f1dc492de..000000000
--- a/deployment/Dockerfile.fedora.amd64
+++ /dev/null
@@ -1,39 +0,0 @@
-FROM fedora:39
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV IS_DOCKER=YES
-
-# Prepare Fedora environment
-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 \
- && dnf clean all \
- && rm -rf /var/cache/dnf
-
-# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/85bcc525-4e9c-471e-9c1d-96259aa1a315/930833ef34f66fe9ee2643b0ba21621a/dotnet-sdk-8.0.201-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
-
-# Create symlinks and directories
-RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \
- && mkdir -p ${SOURCE_DIR}/SPECS \
- && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
- && mkdir -p ${SOURCE_DIR}/SOURCES \
- && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
deleted file mode 100644
index 6b8de3773..000000000
--- a/deployment/Dockerfile.linux.amd64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts unzip \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
deleted file mode 100644
index 49d98da2a..000000000
--- a/deployment/Dockerfile.linux.amd64-musl
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts unzip \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64-musl /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
deleted file mode 100644
index aba33c8b2..000000000
--- a/deployment/Dockerfile.linux.arm64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=arm64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts unzip \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.arm64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
deleted file mode 100644
index 247f75615..000000000
--- a/deployment/Dockerfile.linux.armhf
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=armhf
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts unzip \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.armhf /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64
deleted file mode 100644
index a6e1ba217..000000000
--- a/deployment/Dockerfile.linux.musl-linux-arm64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=arm64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts unzip \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.musl-linux-arm64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64
deleted file mode 100644
index 45980c363..000000000
--- a/deployment/Dockerfile.macos.amd64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.macos.amd64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64
deleted file mode 100644
index ee3a813dd..000000000
--- a/deployment/Dockerfile.macos.arm64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.macos.arm64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
deleted file mode 100644
index 0ab1b1914..000000000
--- a/deployment/Dockerfile.portable
+++ /dev/null
@@ -1,32 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
deleted file mode 100644
index 2326d3e85..000000000
--- a/deployment/Dockerfile.ubuntu.amd64
+++ /dev/null
@@ -1,33 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-jammy
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg wget ca-certificates devscripts \
- mmv build-essential libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.amd64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
deleted file mode 100644
index 461a287a1..000000000
--- a/deployment/Dockerfile.ubuntu.arm64
+++ /dev/null
@@ -1,56 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-jammy
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg wget ca-certificates devscripts \
- mmv build-essential lsb-release
-
-# Prepare the cross-toolchain
-RUN rm /etc/apt/sources.list \
- && export CODENAME="$( lsb_release -c -s )" \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
- && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
- && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
- && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \
- && dpkg --add-architecture arm64 \
- && apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 12 \
- && cd cross-gcc-packages-amd64/cross-gcc-12-arm64 \
- && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
- && apt-get install --no-install-recommends -yqq \
- gcc-12-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu \
- bison flex libtool gdb sharutils netbase libmpc-dev \
- libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev \
- zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 \
- libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust1:arm64 libstdc++6:arm64 libssl-dev:arm64 \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.arm64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
deleted file mode 100644
index 83fe32acf..000000000
--- a/deployment/Dockerfile.ubuntu.armhf
+++ /dev/null
@@ -1,56 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-jammy
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV ARCH=amd64
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg wget ca-certificates devscripts \
- mmv build-essential lsb-release
-
-# Prepare the cross-toolchain
-RUN rm /etc/apt/sources.list \
- && export CODENAME="$( lsb_release -c -s )" \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \
- && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \
- && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \
- && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \
- && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \
- && dpkg --add-architecture armhf \
- && apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 12 \
- && cd cross-gcc-packages-amd64/cross-gcc-12-armhf \
- && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
- && apt-get install --no-install-recommends -yqq \
- gcc-12-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf \
- bison flex libtool gdb sharutils netbase libmpc-dev \
- libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev \
- zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \
- libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust1:armhf libstdc++6:armhf libssl-dev:armhf \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
deleted file mode 100644
index 358fb620a..000000000
--- a/deployment/Dockerfile.windows.amd64
+++ /dev/null
@@ -1,32 +0,0 @@
-ARG DOTNET_VERSION=8.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim
-
-# Docker build arguments
-ARG SOURCE_DIR=/jellyfin
-ARG ARTIFACT_DIR=/dist
-
-# Docker run environment
-ENV SOURCE_DIR=/jellyfin
-ENV ARTIFACT_DIR=/dist
-ENV DEB_BUILD_OPTIONS=noddebs
-ENV IS_DOCKER=YES
-
-# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install --no-install-recommends -yqq \
- debhelper gnupg devscripts unzip \
- mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl3 liblttng-ust1 zip \
- && apt-get clean autoclean -yqq \
- && apt-get autoremove -yqq \
- && rm -rf /var/lib/apt/lists/*
-
-# Link to docker-build script
-RUN ln -sf ${SOURCE_DIR}/deployment/build.windows.amd64 /build.sh
-
-VOLUME ${SOURCE_DIR}/
-
-VOLUME ${ARTIFACT_DIR}/
-
-ENTRYPOINT ["/build.sh"]
diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64
deleted file mode 100755
index 26be377f1..000000000
--- a/deployment/build.centos.amd64
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/bash
-
-#= CentOS/RHEL 9+ amd64 .rpm
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove BuildRequires for dotnet, since it's installed manually
- pushd centos
-
- cp -a jellyfin.spec /tmp/spec.orig
- sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
-
- popd
-fi
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd centos
-
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin.spec
- sed -i "/%changelog/q" jellyfin.spec
-
- cat <<EOF >>jellyfin.spec
-* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
-- Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-EOF
- popd
-fi
-
-# Build RPM
-make -f centos/Makefile srpm outdir=/root/rpmbuild/SRPMS
-rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
-
-# Move the artifacts out
-mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-rm -f centos/jellyfin*.tar.gz
-
-if [[ ${IS_DOCKER} == YES ]]; then
- pushd centos
-
- cp -a /tmp/spec.orig jellyfin.spec
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-
- popd
-fi
-
-popd
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
deleted file mode 100755
index 350b22a85..000000000
--- a/deployment/build.debian.amd64
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-
-#= Debian 12+ amd64 .deb
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd debian
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- cat <<EOF >changelog
-jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
-
- * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-EOF
- popd
-fi
-
-# Build DEB
-dpkg-buildpackage -us -uc --pre-clean --post-clean
-
-mkdir -p "${ARTIFACT_DIR}/"
-mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- cp -a /tmp/control.orig debian/control
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
deleted file mode 100755
index 0dfca0ab4..000000000
--- a/deployment/build.debian.arm64
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-
-#= Debian 12+ arm64 .deb
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd debian
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- cat <<EOF >changelog
-jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
-
- * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-EOF
- popd
-fi
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean
-
-mkdir -p "${ARTIFACT_DIR}/"
-mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- cp -a /tmp/control.orig debian/control
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
deleted file mode 100755
index 0ab9e2f9a..000000000
--- a/deployment/build.debian.armhf
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-
-#= Debian 12+ arm64 .deb
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd debian
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- cat <<EOF >changelog
-jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
-
- * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-EOF
- popd
-fi
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean
-
-mkdir -p "${ARTIFACT_DIR}/"
-mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- cp -a /tmp/control.orig debian/control
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
deleted file mode 100755
index 2b4ec2a9c..000000000
--- a/deployment/build.fedora.amd64
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/bash
-
-#= Fedora 39+ amd64 .rpm
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove BuildRequires for dotnet, since it's installed manually
- pushd fedora
-
- cp -a jellyfin.spec /tmp/spec.orig
- sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
-
- popd
-fi
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd fedora
-
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin.spec
- sed -i "/%changelog/q" jellyfin.spec
-
- cat <<EOF >>jellyfin.spec
-* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
-- Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-EOF
- popd
-fi
-
-# Build RPM
-make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
-rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
-
-# Move the artifacts out
-mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-rm -f fedora/jellyfin*.tar.gz
-
-if [[ ${IS_DOCKER} == YES ]]; then
- pushd fedora
-
- cp -a /tmp/spec.orig jellyfin.spec
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-
- popd
-fi
-
-popd
diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64
deleted file mode 100755
index 2998d2f9e..000000000
--- a/deployment/build.linux.amd64
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= Generic Linux amd64 .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_linux-amd64.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.linux.amd64-musl b/deployment/build.linux.amd64-musl
deleted file mode 100755
index 0fa176465..000000000
--- a/deployment/build.linux.amd64-musl
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= Generic Linux amd64-musl .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_linux-amd64-musl.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64
deleted file mode 100755
index dc44ca330..000000000
--- a/deployment/build.linux.arm64
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= Generic Linux arm64 .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_linux-arm64.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf
deleted file mode 100755
index f9de9ff0a..000000000
--- a/deployment/build.linux.armhf
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= Generic Linux armhf .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_linux-armhf.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.linux.musl-linux-arm64 b/deployment/build.linux.musl-linux-arm64
deleted file mode 100755
index ae9ab010f..000000000
--- a/deployment/build.linux.musl-linux-arm64
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= Generic Linux musl-linux-arm64 .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-arm64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_linux-arm64-musl.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.macos.amd64 b/deployment/build.macos.amd64
deleted file mode 100755
index 81e0f43f6..000000000
--- a/deployment/build.macos.amd64
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= macOS 10.13+ amd64 .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_macos-amd64.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.macos.arm64 b/deployment/build.macos.arm64
deleted file mode 100755
index 0a6f37ede..000000000
--- a/deployment/build.macos.arm64
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= macOS 11.0+ arm64 .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-arm64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-tar -czf jellyfin-server_"${version}"_macos-arm64.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.portable b/deployment/build.portable
deleted file mode 100755
index fad14fccf..000000000
--- a/deployment/build.portable
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-#= Portable .NET DLL .tar.gz
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-# Build archives
-dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=false
-tar -czf jellyfin-server_"${version}"_portable.tar.gz -C dist jellyfin-server_"${version}"
-rm -rf dist/jellyfin-server_"${version}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
deleted file mode 100755
index 6fd87a3ae..000000000
--- a/deployment/build.ubuntu.amd64
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-
-#= Ubuntu 22.04+ amd64 .deb
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd debian
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- cat <<EOF >changelog
-jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
-
- * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-EOF
- popd
-fi
-
-# Build DEB
-dpkg-buildpackage -us -uc --pre-clean --post-clean
-
-mkdir -p "${ARTIFACT_DIR}/"
-mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- cp -a /tmp/control.orig debian/control
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
deleted file mode 100755
index f783941c7..000000000
--- a/deployment/build.ubuntu.arm64
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-
-#= Ubuntu 22.04+ arm64 .deb
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd debian
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- cat <<EOF >changelog
-jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
-
- * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-EOF
- popd
-fi
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean
-
-mkdir -p "${ARTIFACT_DIR}/"
-mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- cp -a /tmp/control.orig debian/control
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
deleted file mode 100755
index cde6708c5..000000000
--- a/deployment/build.ubuntu.armhf
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-
-#= Ubuntu 22.04+ arm64 .deb
-
-set -o errexit
-set -o xtrace
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Modify changelog to unstable configuration if IS_UNSTABLE
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd debian
- PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
-
- cat <<EOF >changelog
-jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
-
- * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
-
- -- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
-EOF
- popd
-fi
-
-# Build DEB
-export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
-dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean
-
-mkdir -p "${ARTIFACT_DIR}/"
-mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- cp -a /tmp/control.orig debian/control
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
deleted file mode 100755
index cd07f4e0b..000000000
--- a/deployment/build.windows.amd64
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/bin/bash
-
-#= Windows 7+ amd64 (x64) .zip
-
-set -o errexit
-set -o xtrace
-
-# Version variables
-NSSM_VERSION="nssm-2.24-101-g897c7ad"
-NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg-portable_win64.zip";
-
-# Move to source directory
-pushd "${SOURCE_DIR}"
-
-# Get version
-if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- version="${BUILD_ID}"
-else
- version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
-fi
-
-output_dir="dist/jellyfin-server_${version}"
-
-# Build binary
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output "${output_dir}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
-
-# Prepare addins
-addin_build_dir="$( mktemp -d )"
-wget ${NSSM_URL} -O "${addin_build_dir}"/nssm.zip
-wget ${FFMPEG_URL} -O "${addin_build_dir}"/jellyfin-ffmpeg.zip
-unzip "${addin_build_dir}"/nssm.zip -d "${addin_build_dir}"
-cp "${addin_build_dir}"/${NSSM_VERSION}/win64/nssm.exe "${output_dir}"/nssm.exe
-unzip "${addin_build_dir}"/jellyfin-ffmpeg.zip -d "${addin_build_dir}"/jellyfin-ffmpeg
-cp "${addin_build_dir}"/jellyfin-ffmpeg/* "${output_dir}"
-rm -rf "${addin_build_dir}"
-
-# Create zip package
-pushd dist
-zip -qr jellyfin-server_"${version}".portable.zip jellyfin-server_"${version}"
-popd
-rm -rf "${output_dir}"
-
-# Move the artifacts out
-mkdir -p "${ARTIFACT_DIR}/"
-mv dist/jellyfin[-_]*.zip "${ARTIFACT_DIR}/"
-
-if [[ ${IS_DOCKER} == YES ]]; then
- chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
-fi
-
-popd
diff --git a/fedora/.gitignore b/fedora/.gitignore
deleted file mode 100644
index 6019b98c2..000000000
--- a/fedora/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*.rpm
-*.zip
-*.tar.gz \ No newline at end of file
diff --git a/fedora/Makefile b/fedora/Makefile
deleted file mode 100644
index 3188cf603..000000000
--- a/fedora/Makefile
+++ /dev/null
@@ -1,52 +0,0 @@
-DIR := $(dir $(lastword $(MAKEFILE_LIST)))
-INSTGIT := $(shell if [ "$$(id -u)" = "0" ]; then dnf -y install git; fi)
-NAME := jellyfin-server
-VERSION := $(shell sed -ne '/^Version:/s/.* *//p' $(DIR)/jellyfin.spec)
-RELEASE := $(shell sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)/jellyfin.spec)
-SRPM := jellyfin-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
-TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
-
-epel-7-x86_64_repos := https://packages.microsoft.com/rhel/7/prod/
-
-fed_ver := $(shell rpm -E %fedora)
-# fallback when not running on Fedora
-fed_ver ?= 36
-TARGET ?= fedora-$(fed_ver)-x86_64
-
-outdir ?= $(PWD)/$(DIR)/
-
-srpm: $(DIR)/$(SRPM)
-tarball: $(DIR)/$(TARBALL)
-
-$(DIR)/$(TARBALL):
- cd $(DIR)/; \
- SOURCE_DIR=.. \
- WORKDIR="$${PWD}"; \
- version=$(VERSION); \
- tar \
- --transform "s,^\.,$(NAME)-$(subst -,~,$(VERSION))," \
- --exclude='.git*' \
- --exclude='**/.git' \
- --exclude='**/.hg' \
- --exclude='**/.vs' \
- --exclude='**/.vscode' \
- --exclude=deployment \
- --exclude='**/bin' \
- --exclude='**/obj' \
- --exclude='**/.nuget' \
- --exclude='*.deb' \
- --exclude='*.rpm' \
- --exclude=$(notdir $@) \
- -czf $(notdir $@) \
- -C $${SOURCE_DIR} ./
-
-$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin.spec
- cd $(DIR)/; \
- rpmbuild -bs jellyfin.spec \
- --define "_sourcedir $$PWD/" \
- --define "_srcrpmdir $(outdir)"
-
-rpms: $(DIR)/$(SRPM)
- mock $(addprefix --addrepo=, $($(TARGET)_repos)) \
- --enable-network \
- -r $(TARGET) $<
diff --git a/fedora/README.md b/fedora/README.md
deleted file mode 100644
index 6ea87740f..000000000
--- a/fedora/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Jellyfin RPM
-
-## Build Fedora Package with docker
-
-Change into this directory `cd rpm-package`
-Run the build script `./build-fedora-rpm.sh`.
-Resulting RPM and src.rpm will be in `../../jellyfin-*.rpm`
-
-## ffmpeg
-
-The RPM package for Fedora/CentOS requires some additional repositories as ffmpeg is not in the main repositories.
-
-```shell
-# ffmpeg from RPMfusion free
-# Fedora
-$ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
-# CentOS 8
-$ sudo dnf localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm
-# CentOS 9
-$ sudo dnf localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-9.noarch.rpm
-```
-
-## Building with dotnet
-
-Jellyfin is build with `--self-contained` so no dotnet required for runtime.
-
-```shell
-# dotnet required for building the RPM
-# Fedora
-$ sudo dnf copr enable @dotnet-sig/dotnet
-# CentOS 8
-$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/8/packages-microsoft-prod.rpm
-# CentOS 9
-$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/9/packages-microsoft-prod.rpm
-```
-
-## TODO
-
-- [ ] OpenSUSE
diff --git a/fedora/jellyfin-firewalld.xml b/fedora/jellyfin-firewalld.xml
deleted file mode 100644
index 538c5d65f..000000000
--- a/fedora/jellyfin-firewalld.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<service>
- <short>Jellyfin</short>
- <description>The Free Software Media System.</description>
- <port protocol="tcp" port="8096"/>
- <port protocol="tcp" port="8920"/>
- <port protocol="udp" port="1900"/>
- <port protocol="udp" port="7359"/>
-</service>
diff --git a/fedora/jellyfin-selinux-launcher.sh b/fedora/jellyfin-selinux-launcher.sh
deleted file mode 100644
index e07a351d9..000000000
--- a/fedora/jellyfin-selinux-launcher.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-exec /usr/lib64/jellyfin/jellyfin "${@}"
diff --git a/fedora/jellyfin-server-lowports.conf b/fedora/jellyfin-server-lowports.conf
deleted file mode 100644
index eeb48a4e4..000000000
--- a/fedora/jellyfin-server-lowports.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-# This allows Jellyfin to bind to low ports such as 80 and/or 443
-
-[Service]
-AmbientCapabilities=CAP_NET_BIND_SERVICE \ No newline at end of file
diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env
deleted file mode 100644
index cee8f6854..000000000
--- a/fedora/jellyfin.env
+++ /dev/null
@@ -1,44 +0,0 @@
-# Jellyfin default configuration options
-
-# Use this file to override the default configurations; add additional
-# options with JELLYFIN_ADD_OPTS.
-
-# To override the user or this config file's location, use
-# /etc/systemd/system/jellyfin.service.d/override.conf
-
-#
-# This is a POSIX shell fragment
-#
-
-#
-# General options
-#
-
-# Program directories
-JELLYFIN_DATA_DIR="/var/lib/jellyfin"
-JELLYFIN_CONFIG_DIR="/etc/jellyfin"
-JELLYFIN_LOG_DIR="/var/log/jellyfin"
-JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
-
-# web client path, installed by the jellyfin-web package
-# JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
-
-# In-App service control
-JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
-
-# Disable glibc dynamic heap adjustment
-MALLOC_TRIM_THRESHOLD_=131072
-
-# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
-#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
-
-# [OPTIONAL] run Jellyfin as a headless service
-#JELLYFIN_SERVICE_OPT="--service"
-
-# [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
-
-# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
-# 0 = Workstation
-# 1 = Server
-#COMPlus_gcServer=1
diff --git a/fedora/jellyfin.override.conf b/fedora/jellyfin.override.conf
deleted file mode 100644
index 48b4de1e9..000000000
--- a/fedora/jellyfin.override.conf
+++ /dev/null
@@ -1,53 +0,0 @@
-# Jellyfin systemd configuration options
-
-# Use this file to override the user or environment file location.
-
-[Service]
-#User = jellyfin
-#EnvironmentFile = /etc/sysconfig/jellyfin
-
-# Service hardening options
-# These were added in PR #6953 to solve issue #6952, but some combination of
-# them causes "restart.sh" functionality to break with the following error:
-# sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the
-# 'nosuid' option set or an NFS file system without root privileges?
-# See issue #7503 for details on the troubleshooting that went into this.
-# Since these were added for NixOS specifically and are above and beyond
-# what 99% of systemd units do, they have been moved here as optional
-# additional flags to set for maximum system security and can be enabled at
-# the administrator's or package maintainer's discretion.
-# Uncomment these only if you know what you're doing, and doing so may cause
-# bugs with in-server Restart and potentially other functionality as well.
-#NoNewPrivileges=true
-#SystemCallArchitectures=native
-#RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
-#RestrictNamespaces=false
-#RestrictRealtime=true
-#RestrictSUIDSGID=true
-#ProtectClock=true
-#ProtectControlGroups=false
-#ProtectHostname=true
-#ProtectKernelLogs=false
-#ProtectKernelModules=false
-#ProtectKernelTunables=false
-#LockPersonality=true
-#PrivateTmp=false
-#PrivateDevices=false
-#PrivateUsers=true
-#RemoveIPC=true
-#SystemCallFilter=~@clock
-#SystemCallFilter=~@aio
-#SystemCallFilter=~@chown
-#SystemCallFilter=~@cpu-emulation
-#SystemCallFilter=~@debug
-#SystemCallFilter=~@keyring
-#SystemCallFilter=~@memlock
-#SystemCallFilter=~@module
-#SystemCallFilter=~@mount
-#SystemCallFilter=~@obsolete
-#SystemCallFilter=~@privileged
-#SystemCallFilter=~@raw-io
-#SystemCallFilter=~@reboot
-#SystemCallFilter=~@setuid
-#SystemCallFilter=~@swap
-#SystemCallErrorNumber=EPERM
diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service
deleted file mode 100644
index 01accdc0c..000000000
--- a/fedora/jellyfin.service
+++ /dev/null
@@ -1,17 +0,0 @@
-[Unit]
-Description = Jellyfin Media Server
-After = network-online.target
-
-[Service]
-Type = simple
-EnvironmentFile = /etc/sysconfig/jellyfin
-User = jellyfin
-Group = jellyfin
-WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
-Restart = on-failure
-TimeoutSec = 15
-SuccessExitStatus=0 143
-
-[Install]
-WantedBy = multi-user.target
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
deleted file mode 100644
index 5327495ad..000000000
--- a/fedora/jellyfin.spec
+++ /dev/null
@@ -1,197 +0,0 @@
-%global debug_package %{nil}
-
-Name: jellyfin
-Version: 10.9.0
-Release: 1%{?dist}
-Summary: The Free Software Media System
-License: GPLv2
-URL: https://jellyfin.org
-# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
-Source0: jellyfin-server-%{version}.tar.gz
-Source10: jellyfin-selinux-launcher.sh
-Source11: jellyfin.service
-Source12: jellyfin.env
-Source13: jellyfin.override.conf
-Source14: jellyfin-firewalld.xml
-Source15: jellyfin-server-lowports.conf
-
-%{?systemd_requires}
-BuildRequires: systemd
-BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
-# Requirements not packaged in RHEL 7 main repos, added via Makefile
-# https://packages.microsoft.com/rhel/7/prod/
-BuildRequires: dotnet-runtime-8.0, dotnet-sdk-8.0
-Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
-
-%description
-Jellyfin is a free software media system that puts you in control of managing and streaming your media.
-
-%package server
-# RPMfusion free
-Summary: The Free Software Media System Server backend
-Requires(pre): shadow-utils
-Requires: ffmpeg
-Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu
-
-%description server
-The Jellyfin media server backend.
-
-%package server-lowports
-# RPMfusion free
-Summary: The Free Software Media System Server backend. Low-port binding.
-Requires: jellyfin-server
-
-%description server-lowports
-The Jellyfin media server backend low port binding package. This package
-enables binding to ports < 1024. You would install this if you want
-the Jellyfin server to bind to ports 80 and/or 443 for example.
-
-%prep
-%autosetup -n jellyfin-server-%{version} -b 0
-
-
-%build
-export DOTNET_CLI_TELEMETRY_OPTOUT=1
-export PATH=$PATH:/usr/local/bin
-# cannot use --output due to https://github.com/dotnet/sdk/issues/22220
-dotnet publish --configuration Release --self-contained --runtime linux-x64 \
- -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
-
-
-%install
-# Jellyfin files
-%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
-%{__cp} -r Jellyfin.Server/bin/Release/net8.0/linux-x64/publish/* %{buildroot}%{_libdir}/jellyfin
-%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
-sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
-
-# Jellyfin config
-%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
-%{__install} -D %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
-
-# system config
-%{__install} -D %{SOURCE14} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
-%{__install} -D %{SOURCE13} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
-%{__install} -D %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
-
-# empty directories
-%{__mkdir} -p %{buildroot}%{_sharedstatedir}/jellyfin
-%{__mkdir} -p %{buildroot}%{_sysconfdir}/jellyfin
-%{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin
-%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
-
-# jellyfin-server-lowports subpackage
-%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
-
-
-%files
-# empty as this is just a meta-package
-
-%files server
-%defattr(644,root,root,755)
-
-# Jellyfin files
-%{_bindir}/jellyfin
-# Needs 755 else only root can run it since binary build by dotnet is 722
-%attr(755,root,root) %{_libdir}/jellyfin/createdump
-%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
-%{_libdir}/jellyfin/*
-%attr(755,root,root) %{_bindir}/jellyfin
-
-# Jellyfin config
-%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
-%config %{_sysconfdir}/sysconfig/jellyfin
-
-# system config
-%{_prefix}/lib/firewalld/services/jellyfin.xml
-%{_unitdir}/jellyfin.service
-%config(noreplace) %{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
-
-# empty directories
-%attr(750,jellyfin,jellyfin) %dir %{_sharedstatedir}/jellyfin
-%attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/jellyfin
-%attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin
-%attr(-, jellyfin,jellyfin) %dir %{_var}/log/jellyfin
-
-%license LICENSE
-
-
-%files server-lowports
-%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
-
-%pre server
-getent group jellyfin >/dev/null || groupadd -r jellyfin
-getent passwd jellyfin >/dev/null || \
- useradd -r -g jellyfin -d %{_sharedstatedir}/jellyfin -s /sbin/nologin \
- -c "Jellyfin default user" jellyfin
-# Add jellyfin to the render and video groups for hwa.
-[ ! -z "$(getent group render)" ] && usermod -aG render jellyfin >/dev/null 2>&1
-[ ! -z "$(getent group video)" ] && usermod -aG video jellyfin >/dev/null 2>&1
-exit 0
-
-%post server
-# Move existing configuration cache and logs to their new locations and symlink them.
-if [ $1 -gt 1 ] ; then
- service_state=$(systemctl is-active jellyfin.service)
- if [ "${service_state}" = "active" ]; then
- systemctl stop jellyfin.service
- fi
- if [ ! -L %{_sharedstatedir}/jellyfin/config ]; then
- mv %{_sharedstatedir}/jellyfin/config/* %{_sysconfdir}/jellyfin/
- rmdir %{_sharedstatedir}/jellyfin/config
- ln -sf %{_sysconfdir}/jellyfin %{_sharedstatedir}/jellyfin/config
- fi
- if [ ! -L %{_sharedstatedir}/jellyfin/logs ]; then
- mv %{_sharedstatedir}/jellyfin/logs/* %{_var}/log/jellyfin
- rmdir %{_sharedstatedir}/jellyfin/logs
- ln -sf %{_var}/log/jellyfin %{_sharedstatedir}/jellyfin/logs
- fi
- if [ ! -L %{_sharedstatedir}/jellyfin/cache ]; then
- mv %{_sharedstatedir}/jellyfin/cache/* %{_var}/cache/jellyfin
- rmdir %{_sharedstatedir}/jellyfin/cache
- ln -sf %{_var}/cache/jellyfin %{_sharedstatedir}/jellyfin/cache
- fi
- if [ "${service_state}" = "active" ]; then
- systemctl start jellyfin.service
- fi
-fi
-%systemd_post jellyfin.service
-
-%preun server
-%systemd_preun jellyfin.service
-
-%postun server
-%systemd_postun_with_restart jellyfin.service
-
-%changelog
-* Wed Jul 13 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.9.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.9.0
-* Mon Nov 29 2021 Brian J. Murrell <brian@interlinx.bc.ca>
-- Add jellyfin-server-lowports.service drop-in in a server-lowports
- subpackage to allow binding to low ports
-* Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
-- Forthcoming stable release
-* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
-- Forthcoming stable release
-* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
-- Forthcoming stable release
-* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
-* Sat Aug 31 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
-* Wed Jul 24 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
-* Sat Jul 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
-* Sun Jun 09 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
-* Thu Jun 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
-* Fri May 17 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
-* Tue Apr 30 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
-* Sat Apr 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
-* Fri Apr 19 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
-- New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 10225e3af..db116f46c 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -85,6 +85,8 @@
<Rule Id="CA1309" Action="Error" />
<!-- error on CA1310: Specify StringComparison for correctness -->
<Rule Id="CA1310" Action="Error" />
+ <!-- error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance -->
+ <Rule Id="CA1513" Action="Error" />
<!-- error on CA1725: Parameter names should match base declaration -->
<Rule Id="CA1725" Action="Error" />
<!-- error on CA1725: Call async methods when in an async method -->
@@ -101,6 +103,8 @@
<Rule Id="CA1849" Action="Error" />
<!-- error on CA1851: Possible multiple enumerations of IEnumerable collection -->
<Rule Id="CA1851" Action="Error" />
+ <!-- error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup -->
+ <Rule Id="CA1854" Action="Error" />
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
@@ -108,6 +112,8 @@
<Rule Id="CA2201" Action="Error" />
<!-- error on CA2215: Dispose methods should call base class dispose -->
<Rule Id="CA2215" Action="Error" />
+ <!-- error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability -->
+ <Rule Id="CA2249" Action="Error" />
<!-- error on CA2254: Template should be a static expression -->
<Rule Id="CA2254" Action="Error" />
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 92299dd06..75963226a 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -188,7 +188,8 @@ public class SkiaEncoder : IImageEncoder
ArgumentException.ThrowIfNullOrEmpty(path);
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
- if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
return string.Empty;
@@ -558,9 +559,13 @@ public class SkiaEncoder : IImageEncoder
/// <inheritdoc />
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
{
- var splashBuilder = new SplashscreenBuilder(this);
- var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
- splashBuilder.GenerateSplash(posters, backdrops, outputPath);
+ // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
+ if (posters.Count > 0 && backdrops.Count > 0)
+ {
+ var splashBuilder = new SplashscreenBuilder(this);
+ var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
+ splashBuilder.GenerateSplash(posters, backdrops, outputPath);
+ }
}
/// <inheritdoc />
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index c91f5d008..98b567e30 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -29,6 +29,7 @@
<ItemGroup>
<PackageReference Include="Diacritics" />
+ <PackageReference Include="ICU4N.Transliterator" />
</ItemGroup>
</Project>
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs
deleted file mode 100644
index cd582ced6..000000000
--- a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Jellyfin.Extensions.Json.Converters
-{
- /// <summary>
- /// Converts an object to a lowercase string.
- /// </summary>
- /// <typeparam name="T">The object type.</typeparam>
- public class JsonLowerCaseConverter<T> : JsonConverter<T>
- {
- /// <inheritdoc />
- public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- return JsonSerializer.Deserialize<T>(ref reader, options);
- }
-
- /// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
- {
- writer.WriteStringValue(value?.ToString()?.ToLowerInvariant());
- }
- }
-}
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 9d8afc23c..8cfebd594 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Text.RegularExpressions;
+using ICU4N.Text;
namespace Jellyfin.Extensions
{
@@ -8,6 +9,9 @@ namespace Jellyfin.Extensions
/// </summary>
public static partial class StringExtensions
{
+ private static readonly Lazy<Transliterator> _transliterator = new(() => Transliterator.GetInstance(
+ "Any-Latin; Latin-Ascii; Lower; NFD; [:Nonspacing Mark:] Remove; [:Punctuation:] Remove;"));
+
// Matches non-conforming unicode chars
// https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
@@ -96,5 +100,15 @@ namespace Jellyfin.Extensions
return haystack[(pos + 1)..];
}
+
+ /// <summary>
+ /// Returns a transliterated string which only contain ascii characters.
+ /// </summary>
+ /// <param name="text">The string to act on.</param>
+ /// <returns>The transliterated string.</returns>
+ public static string Transliterated(this string text)
+ {
+ return _transliterator.Value.Transliterate(text);
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index 1948a9ab9..cce2911dc 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -570,7 +570,6 @@ namespace Jellyfin.LiveTv.Channels
return new ChannelFeatures(channel.Name, channel.Id)
{
CanFilter = !features.MaxPageSize.HasValue,
- CanSearch = provider is ISearchableChannel,
ContentTypes = features.ContentTypes.ToArray(),
DefaultSortFields = features.DefaultSortFields.ToArray(),
MaxPageSize = features.MaxPageSize,
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
index 92605a1eb..2f4caa386 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
@@ -159,7 +159,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
{
Locations = [customPath],
Name = "Recorded Movies",
- CollectionType = CollectionTypeOptions.Movies
+ CollectionType = CollectionTypeOptions.movies
};
}
@@ -172,7 +172,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
{
Locations = [customPath],
Name = "Recorded Shows",
- CollectionType = CollectionTypeOptions.TvShows
+ CollectionType = CollectionTypeOptions.tvshows
};
}
}
diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
index 3666d342e..8d52151cb 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
@@ -150,7 +150,7 @@ namespace Jellyfin.LiveTv.TunerHosts
{
// Use user-defined user-agent. If there isn't one, make it look like a browser.
httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ?
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" :
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" :
info.UserAgent;
}
diff --git a/src/Jellyfin.Networking/AutoDiscoveryHost.cs b/src/Jellyfin.Networking/AutoDiscoveryHost.cs
index 5624c4ed1..2be57d7a1 100644
--- a/src/Jellyfin.Networking/AutoDiscoveryHost.cs
+++ b/src/Jellyfin.Networking/AutoDiscoveryHost.cs
@@ -78,28 +78,36 @@ public sealed class AutoDiscoveryHost : BackgroundService
private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken)
{
- using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber));
- udpClient.MulticastLoopback = false;
-
- while (!cancellationToken.IsCancellationRequested)
+ try
{
- try
+ using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber));
+ udpClient.MulticastLoopback = false;
+
+ while (!cancellationToken.IsCancellationRequested)
{
- var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
- var text = Encoding.UTF8.GetString(result.Buffer);
- if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+ try
{
- await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
+ var text = Encoding.UTF8.GetString(result.Buffer);
+ if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
+ {
+ await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogError(ex, "Failed to receive data from socket");
}
}
- catch (SocketException ex)
- {
- _logger.LogError(ex, "Failed to receive data from socket");
- }
- catch (OperationCanceledException)
- {
- _logger.LogDebug("Broadcast socket operation cancelled");
- }
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogDebug("Broadcast socket operation cancelled");
+ }
+ catch (Exception ex)
+ {
+ // Exception in this function will prevent the background service from restarting in-process.
+ _logger.LogError(ex, "Unable to bind to {Address}:{Port}", address, PortNumber);
}
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
index 1ea1797ba..3687d7753 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
@@ -67,5 +68,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
await _firstTimeSetupHandler.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
+
+ [Fact]
+ public async Task ShouldAllowAdminApiKeyIfStartupWizardComplete()
+ {
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Role, UserRoles.Administrator)]));
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
+
+ await _firstTimeSetupHandler.HandleAsync(context);
+ Assert.True(context.HasSucceeded);
+ }
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs
index 4fd9fd290..5d86d6bae 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonDefaultStringEnumConverterTests.cs
@@ -15,9 +15,9 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
- [InlineData("\"\"", MediaStreamProtocol.Http)]
- [InlineData("\"Http\"", MediaStreamProtocol.Http)]
- [InlineData("\"Hls\"", MediaStreamProtocol.Hls)]
+ [InlineData("\"\"", MediaStreamProtocol.http)]
+ [InlineData("\"Http\"", MediaStreamProtocol.http)]
+ [InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Direct(string input, MediaStreamProtocol output)
{
var value = JsonSerializer.Deserialize<MediaStreamProtocol>(input, _jsonOptions);
@@ -30,10 +30,10 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
- [InlineData(null, MediaStreamProtocol.Http)]
- [InlineData("\"\"", MediaStreamProtocol.Http)]
- [InlineData("\"Http\"", MediaStreamProtocol.Http)]
- [InlineData("\"Hls\"", MediaStreamProtocol.Hls)]
+ [InlineData(null, MediaStreamProtocol.http)]
+ [InlineData("\"\"", MediaStreamProtocol.http)]
+ [InlineData("\"Http\"", MediaStreamProtocol.http)]
+ [InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum(string? input, MediaStreamProtocol output)
{
input ??= "null";
@@ -51,9 +51,9 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData(null, null)]
- [InlineData("\"\"", MediaStreamProtocol.Http)]
- [InlineData("\"Http\"", MediaStreamProtocol.Http)]
- [InlineData("\"Hls\"", MediaStreamProtocol.Hls)]
+ [InlineData("\"\"", MediaStreamProtocol.http)]
+ [InlineData("\"Http\"", MediaStreamProtocol.http)]
+ [InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Nullable(string? input, MediaStreamProtocol? output)
{
input ??= "null";
@@ -69,8 +69,8 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
- [InlineData(MediaStreamProtocol.Http, MediaStreamProtocol.Http)]
- [InlineData(MediaStreamProtocol.Hls, MediaStreamProtocol.Hls)]
+ [InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
+ [InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
public void Enum_RoundTrip(MediaStreamProtocol input, MediaStreamProtocol output)
{
var inputObj = new TestClass { EnumValue = input };
@@ -87,8 +87,8 @@ public class JsonDefaultStringEnumConverterTests
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
- [InlineData(MediaStreamProtocol.Http, MediaStreamProtocol.Http)]
- [InlineData(MediaStreamProtocol.Hls, MediaStreamProtocol.Hls)]
+ [InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
+ [InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
[InlineData(null, null)]
public void Enum_RoundTrip_Nullable(MediaStreamProtocol? input, MediaStreamProtocol? output)
{
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
deleted file mode 100644
index 16c69ca48..000000000
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using Jellyfin.Extensions.Json.Converters;
-using MediaBrowser.Model.Entities;
-using Xunit;
-
-namespace Jellyfin.Extensions.Tests.Json.Converters
-{
- public class JsonLowerCaseConverterTests
- {
- private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
- {
- Converters =
- {
- new JsonStringEnumConverter()
- }
- };
-
- [Theory]
- [InlineData(null, "{\"CollectionType\":null}")]
- [InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")]
- [InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")]
- public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected)
- {
- Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions));
- }
-
- [Theory]
- [InlineData("{\"CollectionType\":null}", null)]
- [InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)]
- [InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)]
- public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result)
- {
- var res = JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions);
- Assert.NotNull(res);
- Assert.Equal(result, res!.CollectionType);
- }
-
- [Theory]
- [InlineData(null)]
- [InlineData(CollectionTypeOptions.Movies)]
- [InlineData(CollectionTypeOptions.MusicVideos)]
- public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value)
- {
- var res = JsonSerializer.Deserialize<TestContainer>(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions);
- Assert.NotNull(res);
- Assert.Equal(value, res!.CollectionType);
- }
-
- [Theory]
- [InlineData("{\"CollectionType\":null}")]
- [InlineData("{\"CollectionType\":\"movies\"}")]
- [InlineData("{\"CollectionType\":\"musicvideos\"}")]
- public void RoundTrip_String_Correct(string json)
- {
- var res = JsonSerializer.Serialize(JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions), _jsonOptions);
- Assert.Equal(json, res);
- }
-
- private sealed class TestContainer
- {
- public TestContainer(CollectionTypeOptions? collectionType)
- {
- CollectionType = collectionType;
- }
-
- [JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
- public CollectionTypeOptions? CollectionType { get; set; }
- }
- }
-}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index db7e91c6a..e0a7fa3aa 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -17,16 +17,11 @@ namespace Jellyfin.MediaEncoding.Tests
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV611Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV60Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV512Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)]
- [InlineData(EncoderValidatorTestsData.FFmpegV404Output, true)]
+ [InlineData(EncoderValidatorTestsData.FFmpegV432Output, false)]
[InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, true)]
[InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)]
public void ValidateVersionInternalTest(string versionOutput, bool valid)
@@ -38,17 +33,12 @@ namespace Jellyfin.MediaEncoding.Tests
{
public GetFFmpegVersionTestData()
{
+ Add(EncoderValidatorTestsData.FFmpegV611Output, new Version(6, 1, 1));
Add(EncoderValidatorTestsData.FFmpegV60Output, new Version(6, 0));
Add(EncoderValidatorTestsData.FFmpegV512Output, new Version(5, 1, 2));
Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
- Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));
- Add(EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3));
- Add(EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1));
- Add(EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2));
- Add(EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4));
- Add(EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4));
- Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0));
+ Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 4));
Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput, null);
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 89ba42da0..30df94950 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV611Output = @"ffmpeg version n6.1.1-16-g33efa50fa4-20240317 Copyright (c) 2000-2023 the FFmpeg developers
+built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
+configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libaribcaption --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317
+libavutil 58. 29.100 / 58. 29.100
+libavcodec 60. 31.102 / 60. 31.102
+libavformat 60. 16.100 / 60. 16.100
+libavdevice 60. 3.100 / 60. 3.100
+libavfilter 9. 12.100 / 9. 12.100
+libswscale 7. 5.100 / 7. 5.100
+libswresample 4. 12.100 / 4. 12.100
+libpostproc 57. 3.100 / 57. 3.100";
+
public const string FFmpegV60Output = @"ffmpeg version 6.0-Jellyfin Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 12.2.0 (crosstool-NG 1.25.0.90_cf9beb1)
configuration: --prefix=/ffbuild/prefix --pkg-config=pkg-config --pkg-config-flags=--static --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --extra-version=Jellyfin --extra-cflags= --extra-cxxflags= --extra-ldflags= --extra-ldexeflags= --extra-libs= --enable-gpl --enable-version3 --enable-lto --disable-ffplay --disable-debug --disable-doc --disable-ptx-compression --disable-sdl2 --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-amf --enable-chromaprint --enable-libdav1d --enable-dxva2 --enable-d3d11va --enable-libfdk-aac --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvpx --enable-libwebp --enable-libvpl --enable-schannel --enable-libsrt --enable-libsvtav1 --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libzimg --enable-libzvbi
@@ -50,90 +62,17 @@ libswscale 5. 7.100 / 5. 7.100
libswresample 3. 7.100 / 3. 7.100
libpostproc 55. 7.100 / 55. 7.100";
- public const string FFmpegV431Output = @"ffmpeg version n4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
-built with gcc 10.1.0 (GCC)
-configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-avisynth --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmfx --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-librav1e --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3
-libavutil 56. 51.100 / 56. 51.100
-libavcodec 58. 91.100 / 58. 91.100
-libavformat 58. 45.100 / 58. 45.100
-libavdevice 58. 10.100 / 58. 10.100
-libavfilter 7. 85.100 / 7. 85.100
-libswscale 5. 7.100 / 5. 7.100
-libswresample 3. 7.100 / 3. 7.100
-libpostproc 55. 7.100 / 55. 7.100";
-
- public const string FFmpegV43Output = @"ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers
-built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
-configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-vdpau --disable-sdl2 --disable-xlib --enable-gpl --enable-version3 --enable-static --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --arch=amd64 --enable-amf --enable-nvenc --enable-nvdec --enable-vaapi --enable-opencl
-libavutil 56. 51.100 / 56. 51.100
-libavcodec 58. 91.100 / 58. 91.100
-libavformat 58. 45.100 / 58. 45.100
-libavdevice 58. 10.100 / 58. 10.100
-libavfilter 7. 85.100 / 7. 85.100
-libswscale 5. 7.100 / 5. 7.100
-libswresample 3. 7.100 / 3. 7.100
-libpostproc 55. 7.100 / 55. 7.100";
-
- public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers
-built with gcc 9.1.1 (GCC) 20190807
-configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
-libavutil 56. 31.100 / 56. 31.100
-libavcodec 58. 54.100 / 58. 54.100
-libavformat 58. 29.100 / 58. 29.100
-libavdevice 58. 8.100 / 58. 8.100
-libavfilter 7. 57.100 / 7. 57.100
-libswscale 5. 5.100 / 5. 5.100
-libswresample 3. 5.100 / 3. 5.100
-libpostproc 55. 5.100 / 55. 5.100";
-
- public const string FFmpegV42Output = @"ffmpeg version n4.2 Copyright (c) 2000-2019 the FFmpeg developers
-built with gcc 9.1.0 (GCC)
-configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3
-libavutil 56. 31.100 / 56. 31.100
-libavcodec 58. 54.100 / 58. 54.100
-libavformat 58. 29.100 / 58. 29.100
-libavdevice 58. 8.100 / 58. 8.100
-libavfilter 7. 57.100 / 7. 57.100
-libswscale 5. 5.100 / 5. 5.100
-libswresample 3. 5.100 / 3. 5.100
-libpostproc 55. 5.100 / 55. 5.100";
-
- public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
-built with gcc 8 (Raspbian 8.3.0-6+rpi1)
-configuration: --prefix=/usr --extra-version='1~deb10u1' --toolchain=hardened --libdir=/usr/lib/arm-linux-gnueabihf --incdir=/usr/include/arm-linux-gnueabihf --arch=arm --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
-libavutil 56. 22.100 / 56. 22.100
-libavcodec 58. 35.100 / 58. 35.100
-libavformat 58. 20.100 / 58. 20.100
-libavdevice 58. 5.100 / 58. 5.100
-libavfilter 7. 40.101 / 7. 40.101
-libavresample 4. 0. 0 / 4. 0. 0
-libswscale 5. 3.100 / 5. 3.100
-libswresample 3. 3.100 / 3. 3.100
-libpostproc 55. 3.100 / 55. 3.100";
-
- public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers
-built with gcc 8 (Debian 8.3.0-6)
-configuration: --toolchain=hardened --prefix=/usr --target-os=linux --enable-cross-compile --extra-cflags=--static --enable-gpl --enable-static --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-sdl2 --disable-xlib --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --enable-omx --enable-omx-rpi --enable-version3 --enable-vaapi --enable-vdpau --arch=amd64 --enable-nvenc --enable-nvdec
-libavutil 56. 14.100 / 56. 14.100
-libavcodec 58. 18.100 / 58. 18.100
-libavformat 58. 12.100 / 58. 12.100
-libavdevice 58. 3.100 / 58. 3.100
-libavfilter 7. 16.100 / 7. 16.100
-libswscale 5. 1.100 / 5. 1.100
-libswresample 3. 1.100 / 3. 1.100
-libpostproc 55. 1.100 / 55. 1.100";
-
- public const string FFmpegGitUnknownOutput2 = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers
-built with gcc 9.1.1 (GCC) 20190716
-configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
-libavutil 56. 30.100 / 56. 30.100
-libavcodec 58. 53.101 / 58. 53.101
-libavformat 58. 28.102 / 58. 28.102
-libavdevice 58. 7.100 / 58. 7.100
-libavfilter 7. 56.101 / 7. 56.101
-libswscale 5. 4.101 / 5. 4.101
-libswresample 3. 4.100 / 3. 4.100
-libpostproc 55. 4.100 / 55. 4.100";
+ public const string FFmpegGitUnknownOutput2 = @"ffmpeg version N-g01fc3034ee-20240317 Copyright (c) 2000-2023 the FFmpeg developers
+built with gcc 13.2.0 (crosstool-NG 1.26.0.65_ecc5e41)
+configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --enable-shared --disable-static --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --disable-libpulse --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --disable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --disable-frei0r --enable-libgme --enable-libkvazaar --enable-libass --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --disable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --disable-vaapi --enable-libvidstab --disable-vulkan --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags='$FF_CFLAGS' --extra-cxxflags='$FF_CXXFLAGS' --extra-ldflags='$FF_LDFLAGS' --extra-ldexeflags='$FF_LDEXEFLAGS' --extra-libs='$FF_LIBS' --extra-version=20240317
+libavutil 56. 70.100 / 56. 70.100
+libavcodec 58.134.100 / 58.134.100
+libavformat 58. 76.100 / 58. 76.100
+libavdevice 58. 13.100 / 58. 13.100
+libavfilter 7.110.100 / 7.110.100
+libswscale 5. 9.100 / 5. 9.100
+libswresample 3. 9.100 / 3. 9.100
+libpostproc 55. 9.100 / 55. 9.100";
public const string FFmpegGitUnknownOutput = @"ffmpeg version N-45325-gb173e0353-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2018 the FFmpeg developers
built with gcc 6.3.0 (Debian 6.3.0-18+deb9u1) 20170516
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 183997fdb..6d88dbb8e 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -389,21 +389,21 @@ namespace Jellyfin.Model.Tests
// Assert.Equal("webm", val.Container);
Assert.Equal(streamInfo.Container, uri.Extension);
Assert.Equal("stream", uri.Filename);
- Assert.Equal(MediaStreamProtocol.Http, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.http, streamInfo.SubProtocol);
}
else if (transcodeProtocol.Equals("HLS.mp4", StringComparison.Ordinal))
{
Assert.Equal("mp4", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename);
- Assert.Equal(MediaStreamProtocol.Hls, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.hls, streamInfo.SubProtocol);
}
else
{
Assert.Equal("ts", streamInfo.Container);
Assert.Equal("m3u8", uri.Extension);
Assert.Equal("master", uri.Filename);
- Assert.Equal(MediaStreamProtocol.Hls, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.hls, streamInfo.SubProtocol);
}
// Full transcode
@@ -489,7 +489,7 @@ namespace Jellyfin.Model.Tests
}
else if (playMethod is null)
{
- Assert.Equal(MediaStreamProtocol.Http, streamInfo.SubProtocol);
+ Assert.Equal(MediaStreamProtocol.http, streamInfo.SubProtocol);
Assert.Equal("stream", uri.Filename);
Assert.False(streamInfo.EstimateContentLength);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index d1be07aa2..940e3c2b1 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -18,6 +18,12 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
+ [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
+ [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
+ [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
+ [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
+ [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]