aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml2
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.devcontainer/Dev - Server Ffmpeg/devcontainer.json28
-rw-r--r--.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh32
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml49
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--.github/workflows/ci-openapi.yml14
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--.github/workflows/commands.yml10
-rw-r--r--.vscode/extensions.json2
-rw-r--r--.vscode/launch.json12
-rw-r--r--CONTRIBUTORS.md6
-rw-r--r--Directory.Packages.props51
-rw-r--r--Dockerfile66
-rw-r--r--Dockerfile.arm56
-rw-r--r--Dockerfile.arm6454
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs44
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs32
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj1
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs48
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs88
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs71
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitorStartup.cs35
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs49
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs17
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs61
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs3
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs4
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs21
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs25
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs75
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ka.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ky.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json)0
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json20
-rw-r--r--Emby.Server.Implementations/Localization/Core/or.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json86
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs5
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs34
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs3
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs7
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs3
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs3
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs3
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs133
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs5
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs5
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs15
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs30
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs8
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs54
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs3
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs5
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs7
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs12
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs3
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs19
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs7
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs6
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs10
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs8
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs3
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs3
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs2
-rw-r--r--Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs13
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj1
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs140
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs64
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs76
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs3
-rw-r--r--Jellyfin.Server/CoreAppHost.cs4
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj1
-rw-r--r--Jellyfin.Server/Startup.cs12
-rw-r--r--Jellyfin.sln14
-rw-r--r--MediaBrowser.Common/Progress/ActionableProgress.cs37
-rw-r--r--MediaBrowser.Common/Progress/SimpleProgress.cs17
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs3
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs7
-rw-r--r--MediaBrowser.Controller/Drawing/ImageStream.cs42
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs16
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs44
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs11
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs5
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs8
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs2
-rw-r--r--MediaBrowser.Controller/Library/ILibraryMonitor.cs9
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs9
-rw-r--r--MediaBrowser.Controller/LiveTv/IGuideManager.cs26
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs26
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs13
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs7
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHostManager.cs46
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs54
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs77
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs210
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs16
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs586
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs20
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs23
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs11
-rw-r--r--MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs9
-rw-r--r--MediaBrowser.Controller/Plugins/IServerEntryPoint.cs20
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs3
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs28
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs46
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs21
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs19
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs278
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs49
-rw-r--r--MediaBrowser.Model/ClientLog/ClientLogEvent.cs75
-rw-r--r--MediaBrowser.Model/Dlna/DeviceIdentification.cs63
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs439
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfileInfo.cs26
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfileType.cs10
-rw-r--r--MediaBrowser.Model/Dlna/HeaderMatchType.cs11
-rw-r--r--MediaBrowser.Model/Dlna/HttpHeaderInfo.cs19
-rw-r--r--MediaBrowser.Model/Dlna/ResponseProfile.cs51
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs35
-rw-r--r--MediaBrowser.Model/Dlna/XmlAttribute.cs25
-rw-r--r--MediaBrowser.Model/Dto/ImageByNameInfo.cs38
-rw-r--r--MediaBrowser.Model/Entities/SpecialFolder.cs36
-rw-r--r--MediaBrowser.Model/IO/IStreamHelper.cs2
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs12
-rw-r--r--MediaBrowser.Model/Net/SocketReceiveResult.cs32
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs11
-rw-r--r--MediaBrowser.Model/Session/HardwareEncodingType.cs7
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs29
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs11
-rw-r--r--MediaBrowser.XbmcMetadata/EntryPoint.cs78
-rw-r--r--MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs87
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs26
-rw-r--r--README.md27
-rwxr-xr-xbump_version4
-rwxr-xr-xdebian/rules12
-rw-r--r--deployment/Dockerfile.centos.amd6429
-rw-r--r--deployment/Dockerfile.debian.amd6417
-rw-r--r--deployment/Dockerfile.debian.arm6425
-rw-r--r--deployment/Dockerfile.debian.armhf25
-rw-r--r--deployment/Dockerfile.docker.amd649
-rw-r--r--deployment/Dockerfile.docker.arm649
-rw-r--r--deployment/Dockerfile.docker.armhf9
-rw-r--r--deployment/Dockerfile.fedora.amd6425
-rw-r--r--deployment/Dockerfile.linux.amd6413
-rw-r--r--deployment/Dockerfile.linux.amd64-musl13
-rw-r--r--deployment/Dockerfile.linux.arm6413
-rw-r--r--deployment/Dockerfile.linux.armhf13
-rw-r--r--deployment/Dockerfile.linux.musl-linux-arm6415
-rw-r--r--deployment/Dockerfile.macos.amd6413
-rw-r--r--deployment/Dockerfile.macos.arm6413
-rw-r--r--deployment/Dockerfile.portable13
-rw-r--r--deployment/Dockerfile.ubuntu.amd6419
-rw-r--r--deployment/Dockerfile.ubuntu.arm6455
-rw-r--r--deployment/Dockerfile.ubuntu.armhf55
-rw-r--r--deployment/Dockerfile.windows.amd6413
-rwxr-xr-xdeployment/build.centos.amd6416
-rwxr-xr-xdeployment/build.debian.amd6410
-rwxr-xr-xdeployment/build.debian.arm6410
-rwxr-xr-xdeployment/build.debian.armhf10
-rwxr-xr-xdeployment/build.fedora.amd646
-rwxr-xr-xdeployment/build.linux.amd648
-rwxr-xr-xdeployment/build.linux.amd64-musl8
-rwxr-xr-xdeployment/build.linux.arm648
-rwxr-xr-xdeployment/build.linux.armhf8
-rwxr-xr-xdeployment/build.linux.musl-linux-arm648
-rwxr-xr-xdeployment/build.macos.amd648
-rwxr-xr-xdeployment/build.macos.arm648
-rwxr-xr-xdeployment/build.portable8
-rwxr-xr-xdeployment/build.ubuntu.amd6410
-rwxr-xr-xdeployment/build.ubuntu.arm6410
-rwxr-xr-xdeployment/build.ubuntu.armhf10
-rwxr-xr-xdeployment/build.windows.amd6422
-rw-r--r--fedora/README.md12
-rw-r--r--fedora/jellyfin.spec16
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaCodecException.cs44
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs1
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaException.cs38
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs16
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj4
-rw-r--r--src/Jellyfin.Extensions/GuidExtensions.cs26
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs2
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelDynamicMediaSourceProvider.cs (renamed from Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelImageProvider.cs (renamed from Emby.Server.Implementations/Channels/ChannelImageProvider.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs (renamed from Emby.Server.Implementations/Channels/ChannelManager.cs)62
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelPostScanTask.cs (renamed from Emby.Server.Implementations/Channels/ChannelPostScanTask.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs (renamed from Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs)5
-rw-r--r--src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs18
-rw-r--r--src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs24
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/DirectRecorder.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs)3
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs)203
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/EncodedRecorder.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/EpgChannelData.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/IRecorder.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs31
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/NfoConfigurationExtensions.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/RecordingHelper.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs)2
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs (renamed from Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs)2
-rw-r--r--src/Jellyfin.LiveTv/ExclusiveLiveStream.cs (renamed from Emby.Server.Implementations/Library/ExclusiveLiveStream.cs)3
-rw-r--r--src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs37
-rw-r--r--src/Jellyfin.LiveTv/Guide/GuideManager.cs707
-rw-r--r--src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs74
-rw-r--r--src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj24
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs)117
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CastDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DayDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/StationDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs)2
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs (renamed from Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs)2
-rw-r--r--src/Jellyfin.LiveTv/LiveTvDtoService.cs (renamed from Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs)7
-rw-r--r--src/Jellyfin.LiveTv/LiveTvManager.cs (renamed from Emby.Server.Implementations/LiveTv/LiveTvManager.cs)803
-rw-r--r--src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs (renamed from Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs)4
-rw-r--r--src/Jellyfin.LiveTv/RecordingNotifier.cs (renamed from Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs)75
-rw-r--r--src/Jellyfin.LiveTv/StreamHelper.cs (renamed from Emby.Server.Implementations/IO/StreamHelper.cs)32
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs)11
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/Channels.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs)191
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs)3
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/LiveStream.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs)3
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs)18
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs)2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/SharedHttpStream.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs)3
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs175
-rw-r--r--src/Jellyfin.Networking/PortForwardingHost.cs (renamed from src/Jellyfin.Networking/ExternalPortForwarding.cs)71
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs35
-rw-r--r--tests/Jellyfin.LiveTv.Tests/HdHomerunHostTests.cs (renamed from tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs)4
-rw-r--r--tests/Jellyfin.LiveTv.Tests/HdHomerunManagerTests.cs (renamed from tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs)4
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj29
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Listings/XmlTvListingsProviderTests.cs (renamed from tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs)4
-rw-r--r--tests/Jellyfin.LiveTv.Tests/RecordingHelperTests.cs (renamed from tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs)4
-rw-r--r--tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs (renamed from tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs)4
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/discover.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json1
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/discover.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/lineup.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/headends_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineup_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineups_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/metadata_programs_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/programs_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_request.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_live_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json)0
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_offline_response.json (renamed from tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json)0
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs3
306 files changed, 3952 insertions, 4553 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 39f98e063e..b0684c0d4c 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -259,7 +259,7 @@ jobs:
publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists
- - task: NuGetAuthenticate@0
+ - task: NuGetAuthenticate@1
displayName: 'Authenticate to unstable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index c03564f970..81fe5add42 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.0",
+ "version": "8.0.1",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
new file mode 100644
index 0000000000..0b848d9f3c
--- /dev/null
+++ b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
@@ -0,0 +1,28 @@
+{
+ "name": "Development Jellyfin Server - FFmpeg",
+ "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
+ // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
+ "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
+ // reads the extensions list and installs them
+ "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
+ "features": {
+ "ghcr.io/devcontainers/features/dotnet:2": {
+ "version": "none",
+ "dotnetRuntimeVersions": "8.0",
+ "aspNetCoreRuntimeVersions": "8.0"
+ },
+ "ghcr.io/devcontainers-contrib/features/apt-packages:1": {
+ "preserve_apt_list": false,
+ "packages": ["libfontconfig1"]
+ },
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "dockerDashComposeVersion": "v2"
+ },
+ "ghcr.io/devcontainers/features/github-cli:1": {},
+ "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
+ },
+ "hostRequirements": {
+ "memory": "8gb",
+ "cpus": 4
+ }
+}
diff --git a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh b/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
new file mode 100644
index 0000000000..c867ef538c
--- /dev/null
+++ b/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+## configure the following for a manuall install of a specific version from the repo
+
+# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
+
+# sudo apt update
+# sudo apt install -f ./ffmpeg.deb -y
+# rm ffmpeg.deb
+
+
+## Add the jellyfin repo
+sudo apt install curl gnupg -y
+sudo apt-get install software-properties-common -y
+sudo add-apt-repository universe -y
+
+sudo mkdir -p /etc/apt/keyrings
+curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
+export VERSION_OS="$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release )"
+export VERSION_CODENAME="$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )"
+export DPKG_ARCHITECTURE="$( dpkg --print-architecture )"
+cat <<EOF | sudo tee /etc/apt/sources.list.d/jellyfin.sources
+Types: deb
+URIs: https://repo.jellyfin.org/${VERSION_OS}
+Suites: ${VERSION_CODENAME}
+Components: main
+Architectures: ${DPKG_ARCHITECTURE}
+Signed-By: /etc/apt/keyrings/jellyfin.gpg
+EOF
+
+sudo apt update -y
+sudo apt install jellyfin-ffmpeg6 -y
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index 5878028330..b690b82c24 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -6,7 +6,11 @@ body:
- type: markdown
attributes:
value: |
- Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
+ Thanks for taking the time to report an issue. Before submitting a report, please do the following:
+ 1. Please head to our forum or chat rooms and troubleshoot with volunteers if you haven't already. Links can be found here: https://jellyfin.org/contact/
+ 2. Please search the bug tracker for similar issues. If you do find one, please comment there instead of opening a new bug report.
+ 3. If you decide to open a new report, please provide as much detail as possible.
+ 4. Please **ONLY** report **ONE** issue per report. If you are experiencing multiple issues, please open multiple reports.
- type: textarea
id: what-happened
attributes:
@@ -14,14 +18,18 @@ body:
description: Also tell us, what did you expect to happen?
placeholder: |
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
-
- This is my issue.
-
- Steps to Reproduce
- 1. In this environment...
- 2. With this config...
- 3. Run '...'
- 4. See error...
+ If you are using an old release of Jellyfin, please also explain why.
+ validations:
+ required: true
+ - type: textarea
+ id: repro-steps
+ attributes:
+ label: Reproduction Steps
+ placeholder: |
+ 1. In this environment...
+ 2. With this config...
+ 3. Run '...'
+ 4. See error...
validations:
required: true
- type: dropdown
@@ -30,11 +38,10 @@ body:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- - 10.8.z
- - 10.8.9
- - 10.7.7
- - 10.6.4
- - Other
+ - 10.8.13
+ - 10.8.12
+ - 10.8.11 or older (please specify)
+ - Unstable (master branch)
validations:
required: true
- type: input
@@ -77,6 +84,18 @@ body:
- Networking:
- Storage:
render: markdown
+ validations:
+ required: true
+ - type: markdown
+ attributes:
+ value: |
+ When providing logs, please keep the following things in mind.
+ 1. **DO NOT** use external paste services.
+ 2. Please provide complete logs.
+ - For server logs, include everything you think is important plus *10 lines before and after*
+ - For ffmpeg logs, please provide the entire file unmodified.
+ 3. Please do not run logs through any translation program. Especially beware if your browser translates pages by default.
+ 4. Please do not include logs as screenshots, with the only exception being client logs in browsers.
- type: textarea
id: logs
attributes:
@@ -84,6 +103,8 @@ body:
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
render: shell
+ validations:
+ required: true
- type: textarea
id: ffmpeg-logs
attributes:
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 4b2673e82d..839bebb96a 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@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
+ uses: github/codeql-action/init@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
+ uses: github/codeql-action/autobuild@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
+ uses: github/codeql-action/analyze@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index c9f17429a2..97f1a33e76 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: openapi-head
retention-days: 14
@@ -59,7 +59,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: openapi-base
retention-days: 14
@@ -78,12 +78,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
+ uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
+ uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
with:
name: openapi-base
path: openapi-base
@@ -105,14 +105,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
+ uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -127,7 +127,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 0dacbc5c61..4b5db14aef 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@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0
+ uses: danielpalme/ReportGenerator-GitHub-Action@68f1963d9876d2ac78bfd1c41c395514b7318855 # 5.2.1
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 75b6a73e56..386f8d321b 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -58,7 +58,7 @@ jobs:
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
+ uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index d738e9fba4..3be946e446 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -2,7 +2,7 @@
"recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
- "GitHub.vscode-github-actions",
+ "github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit"
],
diff --git a/.vscode/launch.json b/.vscode/launch.json
index be55764fd4..7e50d4f0a4 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -30,6 +30,18 @@
"internalConsoleOptions": "openOnSessionStart"
},
{
+ "name": "ghcs .NET Launch (nowebclient, ffmpeg)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
+ "cwd": "${workspaceFolder}/Jellyfin.Server",
+ "console": "internalConsole",
+ "stopAtEntry": false,
+ "internalConsoleOptions": "openOnSessionStart"
+ },
+ {
"name": ".NET Attach",
"type": "coreclr",
"request": "attach",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 4e45fd24ad..3be6331c8f 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -4,6 +4,7 @@
- [97carmine](https://github.com/97carmine)
- [Abbe98](https://github.com/Abbe98)
- [agrenott](https://github.com/agrenott)
+ - [alltilla](https://github.com/alltilla)
- [AndreCarvalho](https://github.com/AndreCarvalho)
- [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume)
@@ -77,6 +78,7 @@
- [Marenz](https://github.com/Marenz)
- [marius-luca-87](https://github.com/marius-luca-87)
- [mark-monteiro](https://github.com/mark-monteiro)
+ - [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
- [Matt07211](https://github.com/Matt07211)
- [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00)
@@ -172,9 +174,11 @@
- [tallbl0nde](https://github.com/tallbl0nde)
- [sleepycatcoding](https://github.com/sleepycatcoding)
- [scampower3](https://github.com/scampower3)
- - [Chris-Codes-It] (https://github.com/Chris-Codes-It)
+ - [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)
+ - [Gauvino](https://github.com/Gauvino)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6c662d600b..4a236e5b64 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,50 +4,51 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
+ <PackageVersion Include="AsyncKeyedLock" Version="6.3.4" />
<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="BlurHashSharp.SkiaSharp" Version="1.3.0" />
- <PackageVersion Include="BlurHashSharp" Version="1.3.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.0" />
- <PackageVersion Include="Diacritics" Version="3.3.18" />
+ <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.1.1" />
+ <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
- <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.0.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
+ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
<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.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" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.1" />
<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" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.1" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -57,7 +58,7 @@
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
- <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
+ <PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
@@ -66,18 +67,18 @@
<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.5" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
+ <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="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
+ <PackageVersion Include="Svg.Skia" Version="1.0.0.13" />
<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.0" />
+ <PackageVersion Include="System.Text.Json" Version="8.0.1" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.1.0" />
@@ -85,6 +86,6 @@
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageVersion Include="xunit" Version="2.6.5" />
+ <PackageVersion Include="xunit" Version="2.6.6" />
</ItemGroup>
</Project>
diff --git a/Dockerfile b/Dockerfile
index d3f10cd12e..550c3203d7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,72 +8,68 @@ 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:stable-slim as app
+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.0.2
-ARG IGC_VERSION=1.0.10395
-ARG NEO_VERSION=22.08.22549
-ARG LEVEL_ZERO_VERSION=1.3.22549
+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
-# Install dependencies:
-# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
-# curl: healthcheck
RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget curl \
- && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
+ && 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-ffmpeg5 \
- openssl \
- locales \
+ && 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 \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
- && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
- && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
+ && 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 wget -y \
+ && apt-get remove gnupg -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
- && mkdir -p /cache /config /media \
- && chmod 777 /cache /config /media \
+ && 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
+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
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
+
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
FROM app
@@ -83,11 +79,9 @@ COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
-VOLUME /cache /config
-ENTRYPOINT ["./jellyfin/jellyfin", \
- "--datadir", "/config", \
- "--cachedir", "/cache", \
- "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+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
+ CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index db1acc838a..07039e43b5 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -4,64 +4,58 @@
# 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:stable-slim as app
+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
-# curl: setup & healthcheck
RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
- curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
- curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
- echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
- echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
- apt-get update && \
- apt-get install --no-install-recommends --no-install-suggests -y \
- jellyfin-ffmpeg \
- libssl-dev \
- libfontconfig1 \
- libfreetype6 \
- vainfo \
- libva2 \
- locales \
+ && 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 /cache /config /media \
- && chmod 777 /cache /config /media \
+ && 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
+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
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
+
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
FROM app
@@ -72,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
-VOLUME /cache /config
-ENTRYPOINT ["./jellyfin/jellyfin", \
- "--datadir", "/config", \
- "--cachedir", "/cache", \
- "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+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
+ CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 3eb5f45fc4..54023794fc 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -4,58 +4,58 @@
# 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:stable-slim as app
+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
-# curl: healcheck
-RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
- ffmpeg \
- libssl-dev \
- ca-certificates \
- libfontconfig1 \
- libfreetype6 \
- libomxil-bellagio0 \
- libomxil-bellagio-bin \
- locales \
- curl \
+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 /cache /config /media \
- && chmod 777 /cache /config /media \
+ && 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
+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
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
+
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
FROM app
@@ -66,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
-VOLUME /cache /config
-ENTRYPOINT ["./jellyfin/jellyfin", \
- "--datadir", "/config", \
- "--cachedir", "/cache", \
- "--ffmpeg", "/usr/bin/ffmpeg"]
+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
+ CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index f385f6a512..550c16b4c4 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -15,7 +15,6 @@ using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Emby.Photos;
-using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections;
using Emby.Server.Implementations.Configuration;
using Emby.Server.Implementations.Cryptography;
@@ -25,7 +24,6 @@ using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
-using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
@@ -64,7 +62,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
@@ -395,7 +392,7 @@ namespace Emby.Server.Implementations
/// Runs the startup tasks.
/// </summary>
/// <returns><see cref="Task" />.</returns>
- public async Task RunStartupTasksAsync()
+ public Task RunStartupTasksAsync()
{
Logger.LogInformation("Running startup tasks");
@@ -407,38 +404,10 @@ namespace Emby.Server.Implementations
Resolve<IMediaEncoder>().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
-
- var entryPoints = GetExports<IServerEntryPoint>();
-
- var stopWatch = new Stopwatch();
- stopWatch.Start();
-
- await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
- Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
-
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
- stopWatch.Restart();
-
- await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
- Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
- stopWatch.Stop();
- }
-
- private IEnumerable<Task> StartEntryPoints(IEnumerable<IServerEntryPoint> entryPoints, bool isBeforeStartup)
- {
- foreach (var entryPoint in entryPoints)
- {
- if (isBeforeStartup != (entryPoint is IRunBeforeStartup))
- {
- continue;
- }
-
- Logger.LogDebug("Starting entry point {Type}", entryPoint.GetType());
-
- yield return entryPoint.RunAsync();
- }
+ return Task.CompletedTask;
}
/// <inheritdoc/>
@@ -504,8 +473,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_xmlSerializer);
- serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
-
serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
@@ -557,8 +524,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
serviceCollection.AddSingleton<IDtoService, DtoService>();
- serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
-
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
@@ -567,9 +532,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
- serviceCollection.AddSingleton<LiveTvDtoService>();
- serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
-
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
@@ -704,8 +666,6 @@ namespace Emby.Server.Implementations
GetExports<IMetadataSaver>(),
GetExports<IExternalId>());
- Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
-
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index d0772654ce..a6336f1451 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -699,7 +699,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBindNull("@EndDate");
}
- saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(default) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
+ saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
if (item is IHasProgramAttributes hasProgramAttributes)
{
@@ -729,7 +729,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
var parentId = item.ParentId;
- if (parentId.Equals(default))
+ if (parentId.IsEmpty())
{
saveItemStatement.TryBindNull("@ParentId");
}
@@ -925,7 +925,7 @@ namespace Emby.Server.Implementations.Data
{
saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
- var nullableSeasonId = episode.SeasonId.Equals(default) ? (Guid?)null : episode.SeasonId;
+ var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId;
saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
}
@@ -937,7 +937,7 @@ namespace Emby.Server.Implementations.Data
if (item is IHasSeries hasSeries)
{
- var nullableSeriesId = hasSeries.SeriesId.Equals(default) ? (Guid?)null : hasSeries.SeriesId;
+ var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId;
saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
@@ -1010,7 +1010,7 @@ namespace Emby.Server.Implementations.Data
}
Guid ownerId = item.OwnerId;
- if (ownerId.Equals(default))
+ if (ownerId.IsEmpty())
{
saveItemStatement.TryBindNull("@OwnerId");
}
@@ -1266,7 +1266,7 @@ namespace Emby.Server.Implementations.Data
/// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
public BaseItem RetrieveItem(Guid id)
{
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
@@ -1970,7 +1970,7 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentNullException(nameof(id));
}
@@ -3230,7 +3230,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"ChannelId in ({inClause})");
}
- if (!query.ParentId.Equals(default))
+ if (!query.ParentId.IsEmpty())
{
whereClauses.Add("ParentId=@ParentId");
statement?.TryBind("@ParentId", query.ParentId);
@@ -4452,7 +4452,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
public void DeleteItem(Guid id)
{
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentNullException(nameof(id));
}
@@ -4583,13 +4583,13 @@ AND Type = @InternalPersonType)");
statement?.TryBind("@UserId", query.User.InternalId);
}
- if (!query.ItemId.Equals(default))
+ if (!query.ItemId.IsEmpty())
{
whereClauses.Add("ItemId=@ItemId");
statement?.TryBind("@ItemId", query.ItemId);
}
- if (!query.AppearsInItemId.Equals(default))
+ if (!query.AppearsInItemId.IsEmpty())
{
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
statement?.TryBind("@AppearsInItemId", query.AppearsInItemId);
@@ -4640,7 +4640,7 @@ AND Type = @InternalPersonType)");
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
{
- if (itemId.Equals(default))
+ if (itemId.IsEmpty())
{
throw new ArgumentNullException(nameof(itemId));
}
@@ -5156,7 +5156,7 @@ AND Type = @InternalPersonType)");
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
{
- if (itemId.Equals(default))
+ if (itemId.IsEmpty())
{
throw new ArgumentNullException(nameof(itemId));
}
@@ -5228,7 +5228,7 @@ AND Type = @InternalPersonType)");
public void UpdatePeople(Guid itemId, List<PersonInfo> people)
{
- if (itemId.Equals(default))
+ if (itemId.IsEmpty())
{
throw new ArgumentNullException(nameof(itemId));
}
@@ -5378,7 +5378,7 @@ AND Type = @InternalPersonType)");
{
CheckDisposed();
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentNullException(nameof(id));
}
@@ -5758,7 +5758,7 @@ AND Type = @InternalPersonType)");
CancellationToken cancellationToken)
{
CheckDisposed();
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty.", nameof(id));
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index b3344bb9ff..34276355a7 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,7 +22,6 @@
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" />
- <PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index a83d7a4105..4c668379c8 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -7,24 +7,25 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints;
/// <summary>
-/// A <see cref="IServerEntryPoint"/> that notifies users when libraries are updated.
+/// A <see cref="IHostedService"/> responsible for notifying users when libraries are updated.
/// </summary>
-public sealed class LibraryChangedNotifier : IServerEntryPoint
+public sealed class LibraryChangedNotifier : IHostedService, IDisposable
{
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
@@ -69,7 +70,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
}
/// <inheritdoc />
- public Task RunAsync()
+ public Task StartAsync(CancellationToken cancellationToken)
{
_libraryManager.ItemAdded += OnLibraryItemAdded;
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
@@ -82,6 +83,20 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
return Task.CompletedTask;
}
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _libraryManager.ItemAdded -= OnLibraryItemAdded;
+ _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
+
+ _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+ _providerManager.RefreshProgress -= OnProviderRefreshProgress;
+
+ return Task.CompletedTask;
+ }
+
private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e)
{
var item = e.Argument.Item1;
@@ -136,9 +151,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
}
private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e)
- {
- OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
- }
+ => OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e)
{
@@ -241,7 +254,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
{
var userIds = _sessionManager.Sessions
.Select(i => i.UserId)
- .Where(i => !i.Equals(default))
+ .Where(i => !i.IsEmpty())
.Distinct()
.ToArray();
@@ -341,7 +354,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
return item.SourceType == SourceType.Library;
}
- private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
+ private static IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
{
var list = new List<string>();
@@ -362,7 +375,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
return list.Distinct(StringComparer.Ordinal);
}
- private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
+ private T[] TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
where T : BaseItem
{
// If the physical root changed, return the user root
@@ -383,18 +396,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
/// <inheritdoc />
public void Dispose()
{
- _libraryManager.ItemAdded -= OnLibraryItemAdded;
- _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
- _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
-
- _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
- _providerManager.RefreshStarted -= OnProviderRefreshStarted;
- _providerManager.RefreshProgress -= OnProviderRefreshProgress;
-
- if (_libraryUpdateTimer is not null)
- {
- _libraryUpdateTimer.Dispose();
- _libraryUpdateTimer = null;
- }
+ _libraryUpdateTimer?.Dispose();
+ _libraryUpdateTimer = null;
}
}
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index d32759017d..957ad9c01b 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -8,14 +6,17 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Hosting;
namespace Emby.Server.Implementations.EntryPoints
{
- public sealed class UserDataChangeNotifier : IServerEntryPoint
+ /// <summary>
+ /// <see cref="IHostedService"/> responsible for notifying users when associated item data is updated.
+ /// </summary>
+ public sealed class UserDataChangeNotifier : IHostedService, IDisposable
{
private const int UpdateDuration = 500;
@@ -23,25 +24,43 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IUserDataManager _userDataManager;
private readonly IUserManager _userManager;
- private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
+ private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
+ private readonly object _syncLock = new();
- private readonly object _syncLock = new object();
private Timer? _updateTimer;
- public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
+ /// </summary>
+ /// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
+ /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+ /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+ public UserDataChangeNotifier(
+ IUserDataManager userDataManager,
+ ISessionManager sessionManager,
+ IUserManager userManager)
{
_userDataManager = userDataManager;
_sessionManager = sessionManager;
_userManager = userManager;
}
- public Task RunAsync()
+ /// <inheritdoc />
+ public Task StartAsync(CancellationToken cancellationToken)
{
_userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
return Task.CompletedTask;
}
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
+
+ return Task.CompletedTask;
+ }
+
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
@@ -103,55 +122,40 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
- }
-
- private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
- {
- foreach ((var key, var value) in changes)
+ foreach (var (userId, changedItems) in changes)
{
- await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
+ await _sessionManager.SendMessageToUserSessions(
+ [userId],
+ SessionMessageType.UserDataChanged,
+ () => GetUserDataChangeInfo(userId, changedItems),
+ default).ConfigureAwait(false);
}
}
- private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
- {
- return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
- }
-
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
{
var user = _userManager.GetUserById(userId);
- var dtoList = changedItems
- .DistinctBy(x => x.Id)
- .Select(i =>
- {
- var dto = _userDataManager.GetUserDataDto(i, user);
- dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
- return dto;
- })
- .ToArray();
-
- var userIdString = userId.ToString("N", CultureInfo.InvariantCulture);
-
return new UserDataChangeInfo
{
- UserId = userIdString,
-
- UserDataList = dtoList
+ UserId = userId.ToString("N", CultureInfo.InvariantCulture),
+ UserDataList = changedItems
+ .DistinctBy(x => x.Id)
+ .Select(i =>
+ {
+ var dto = _userDataManager.GetUserDataDto(i, user);
+ dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
+ return dto;
+ })
+ .ToArray()
};
}
+ /// <inheritdoc />
public void Dispose()
{
- if (_updateTimer is not null)
- {
- _updateTimer.Dispose();
- _updateTimer = null;
- }
-
- _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
+ _updateTimer?.Dispose();
+ _updateTimer = null;
}
}
}
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index dde38906f3..31617d1a5f 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -11,11 +9,13 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
- public class LibraryMonitor : ILibraryMonitor
+ /// <inheritdoc cref="ILibraryMonitor" />
+ public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
{
private readonly ILogger<LibraryMonitor> _logger;
private readonly ILibraryManager _libraryManager;
@@ -25,19 +25,19 @@ namespace Emby.Server.Implementations.IO
/// <summary>
/// The file system watchers.
/// </summary>
- private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The affected paths.
/// </summary>
- private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>();
+ private readonly List<FileRefresher> _activeRefreshers = [];
/// <summary>
/// A dynamic list of paths that should be ignored. Added to during our own file system modifications.
/// </summary>
- private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
- private bool _disposed = false;
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
@@ -46,34 +46,31 @@ namespace Emby.Server.Implementations.IO
/// <param name="libraryManager">The library manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
+ /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
public LibraryMonitor(
ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
- IFileSystem fileSystem)
+ IFileSystem fileSystem,
+ IHostApplicationLifetime appLifetime)
{
_libraryManager = libraryManager;
_logger = logger;
_configurationManager = configurationManager;
_fileSystem = fileSystem;
- }
- /// <summary>
- /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
- /// </summary>
- /// <param name="path">The path.</param>
- private void TemporarilyIgnore(string path)
- {
- _tempIgnoredPaths[path] = path;
+ appLifetime.ApplicationStarted.Register(Start);
}
+ /// <inheritdoc />
public void ReportFileSystemChangeBeginning(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
- TemporarilyIgnore(path);
+ _tempIgnoredPaths[path] = path;
}
+ /// <inheritdoc />
public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
{
ArgumentException.ThrowIfNullOrEmpty(path);
@@ -107,14 +104,10 @@ namespace Emby.Server.Implementations.IO
var options = _libraryManager.GetLibraryOptions(item);
- if (options is not null)
- {
- return options.EnableRealtimeMonitor;
- }
-
- return false;
+ return options is not null && options.EnableRealtimeMonitor;
}
+ /// <inheritdoc />
public void Start()
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
@@ -306,21 +299,12 @@ namespace Emby.Server.Implementations.IO
{
if (removeFromList)
{
- RemoveWatcherFromList(watcher);
+ _fileSystemWatchers.TryRemove(watcher.Path, out _);
}
}
}
/// <summary>
- /// Removes the watcher from list.
- /// </summary>
- /// <param name="watcher">The watcher.</param>
- private void RemoveWatcherFromList(FileSystemWatcher watcher)
- {
- _fileSystemWatchers.TryRemove(watcher.Path, out _);
- }
-
- /// <summary>
/// Handles the Error event of the watcher control.
/// </summary>
/// <param name="sender">The source of the event.</param>
@@ -352,6 +336,7 @@ namespace Emby.Server.Implementations.IO
}
}
+ /// <inheritdoc />
public void ReportFileSystemChanged(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
@@ -479,31 +464,15 @@ namespace Emby.Server.Implementations.IO
}
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
+ /// <inheritdoc />
public void Dispose()
{
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
if (_disposed)
{
return;
}
- if (disposing)
- {
- Stop();
- }
-
+ Stop();
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs
deleted file mode 100644
index c51cf05459..0000000000
--- a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-
-namespace Emby.Server.Implementations.IO
-{
- /// <summary>
- /// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
- /// </summary>
- public sealed class LibraryMonitorStartup : IServerEntryPoint
- {
- private readonly ILibraryMonitor _monitor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
- /// </summary>
- /// <param name="monitor">The library monitor.</param>
- public LibraryMonitorStartup(ILibraryMonitor monitor)
- {
- _monitor = monitor;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _monitor.Start();
- return Task.CompletedTask;
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index a79ffd9cb1..7998ce34a7 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -22,7 +22,6 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
@@ -732,7 +731,7 @@ namespace Emby.Server.Implementations.Library
Path = path
};
- if (folder.Id.Equals(default))
+ if (folder.Id.IsEmpty())
{
if (string.IsNullOrEmpty(folder.Path))
{
@@ -1022,7 +1021,7 @@ namespace Emby.Server.Implementations.Library
// Start by just validating the children of the root, but go no further
await RootFolder.ValidateChildren(
- new SimpleProgress<double>(),
+ new Progress<double>(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
cancellationToken).ConfigureAwait(false);
@@ -1030,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().ValidateChildren(
- new SimpleProgress<double>(),
+ new Progress<double>(),
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false,
cancellationToken).ConfigureAwait(false);
@@ -1048,18 +1047,14 @@ namespace Emby.Server.Implementations.Library
await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
- var innerProgress = new ActionableProgress<double>();
-
- innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
+ var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
// Validate the entire media library
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
progress.Report(96);
- innerProgress = new ActionableProgress<double>();
-
- innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04)));
+ innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
@@ -1081,12 +1076,10 @@ namespace Emby.Server.Implementations.Library
foreach (var task in tasks)
{
- var innerProgress = new ActionableProgress<double>();
-
// Prevent access to modified closure
var currentNumComplete = numComplete;
- innerProgress.RegisterAction(pct =>
+ var innerProgress = new Progress<double>(pct =>
{
double innerPercent = pct;
innerPercent /= 100;
@@ -1219,7 +1212,7 @@ namespace Emby.Server.Implementations.Library
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
public BaseItem GetItemById(Guid id)
{
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
@@ -1241,7 +1234,7 @@ namespace Emby.Server.Implementations.Library
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
- if (query.Recursive && !query.ParentId.Equals(default))
+ if (query.Recursive && !query.ParentId.IsEmpty())
{
var parent = GetItemById(query.ParentId);
if (parent is not null)
@@ -1272,7 +1265,7 @@ namespace Emby.Server.Implementations.Library
public int GetCount(InternalItemsQuery query)
{
- if (query.Recursive && !query.ParentId.Equals(default))
+ if (query.Recursive && !query.ParentId.IsEmpty())
{
var parent = GetItemById(query.ParentId);
if (parent is not null)
@@ -1430,7 +1423,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
{
- if (query.Recursive && !query.ParentId.Equals(default))
+ if (query.Recursive && !query.ParentId.IsEmpty())
{
var parent = GetItemById(query.ParentId);
if (parent is not null)
@@ -1486,7 +1479,7 @@ namespace Emby.Server.Implementations.Library
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
if (query.AncestorIds.Length == 0 &&
- query.ParentId.Equals(default) &&
+ query.ParentId.IsEmpty() &&
query.ChannelIds.Count == 0 &&
query.TopParentIds.Length == 0 &&
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
@@ -1520,7 +1513,7 @@ namespace Emby.Server.Implementations.Library
}
// Translate view into folders
- if (!view.DisplayParentId.Equals(default))
+ if (!view.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(view.DisplayParentId);
if (displayParent is not null)
@@ -1531,7 +1524,7 @@ namespace Emby.Server.Implementations.Library
return Array.Empty<Guid>();
}
- if (!view.ParentId.Equals(default))
+ if (!view.ParentId.IsEmpty())
{
var displayParent = GetItemById(view.ParentId);
if (displayParent is not null)
@@ -2137,7 +2130,7 @@ namespace Emby.Server.Implementations.Library
return null;
}
- while (!item.ParentId.Equals(default))
+ while (!item.ParentId.IsEmpty())
{
var parent = item.GetParent();
if (parent is null || parent is AggregateFolder)
@@ -2215,7 +2208,7 @@ namespace Emby.Server.Implementations.Library
CollectionType? viewType,
string sortName)
{
- var parentIdString = parentId.Equals(default)
+ var parentIdString = parentId.IsEmpty()
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
@@ -2251,7 +2244,7 @@ namespace Emby.Server.Implementations.Library
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
- if (!refresh && !item.DisplayParentId.Equals(default))
+ if (!refresh && !item.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2315,7 +2308,7 @@ namespace Emby.Server.Implementations.Library
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
- if (!refresh && !item.DisplayParentId.Equals(default))
+ if (!refresh && !item.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2345,7 +2338,7 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(name);
- var parentIdString = parentId.Equals(default)
+ var parentIdString = parentId.IsEmpty()
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
@@ -2391,7 +2384,7 @@ namespace Emby.Server.Implementations.Library
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
- if (!refresh && !item.DisplayParentId.Equals(default))
+ if (!refresh && !item.DisplayParentId.IsEmpty())
{
var displayParent = GetItemById(item.DisplayParentId);
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2419,7 +2412,7 @@ namespace Emby.Server.Implementations.Library
return GetItemById(parentId.Value);
}
- if (userId.HasValue && !userId.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
return GetUserRootFolder();
}
@@ -2954,7 +2947,7 @@ namespace Emby.Server.Implementations.Library
Task.Run(() =>
{
// No need to start if scanning the library because it will handle it
- ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
+ ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
});
}
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 59d705acef..d4aeae41a5 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -48,20 +48,23 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
- FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
- // _logger.LogDebug("Found cached media info");
+ await using (jsonStream.ConfigureAwait(false))
+ {
+ mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ // _logger.LogDebug("Found cached media info");
+ }
}
- catch (Exception ex)
+ catch (IOException ex)
{
- _logger.LogError(ex, "Error deserializing mediainfo cache");
+ _logger.LogDebug(ex, "Could not open cached media info");
}
- finally
+ catch (Exception ex)
{
- await jsonStream.DisposeAsync().ConfigureAwait(false);
+ _logger.LogError(ex, "Error opening cached media info");
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 96fad9bca8..18ada6aeb5 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -11,14 +11,17 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using EasyCaching.Core.Configurations;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
@@ -37,6 +40,7 @@ namespace Emby.Server.Implementations.Library
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimeter = '_';
+ private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
@@ -49,12 +53,13 @@ namespace Emby.Server.Implementations.Library
private readonly IDirectoryService _directoryService;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
- private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers;
public MediaSourceManager(
+ IServerApplicationHost appHost,
IItemRepository itemRepo,
IApplicationPaths applicationPaths,
ILocalizationManager localizationManager,
@@ -66,6 +71,7 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder,
IDirectoryService directoryService)
{
+ _appHost = appHost;
_itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
@@ -463,12 +469,10 @@ namespace Emby.Server.Implementations.Library
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
MediaSourceInfo mediaSource;
ILiveStream liveStream;
- try
+ using (await _liveStreamLocker.LockAsync(cancellationToken).ConfigureAwait(false))
{
var (provider, keyId) = GetProvider(request.OpenToken);
@@ -488,10 +492,6 @@ namespace Emby.Server.Implementations.Library
_openStreams[mediaSource.LiveStreamId] = liveStream;
}
- finally
- {
- _liveStreamSemaphore.Release();
- }
try
{
@@ -520,10 +520,10 @@ namespace Emby.Server.Implementations.Library
_logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
- if (!request.UserId.Equals(default))
+ if (!request.UserId.IsEmpty())
{
var user = _userManager.GetUserById(request.UserId);
- var item = request.ItemId.Equals(default)
+ var item = request.ItemId.IsEmpty()
? null
: _libraryManager.GetItemById(request.ItemId);
SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
@@ -799,13 +799,40 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
+ public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
+ {
+ var stream = new MediaSourceInfo
+ {
+ EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+ EncoderProtocol = MediaProtocol.Http,
+ Path = info.Path,
+ Protocol = MediaProtocol.File,
+ Id = info.Id,
+ SupportsDirectPlay = false,
+ SupportsDirectStream = true,
+ SupportsTranscoding = true,
+ IsInfiniteStream = true,
+ RequiresOpening = false,
+ RequiresClosing = false,
+ BufferMs = 0,
+ IgnoreDts = true,
+ IgnoreIndex = true
+ };
+
+ await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
+ .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
+
+ return new List<MediaSourceInfo>
+ {
+ stream
+ };
+ }
+
public async Task CloseLiveStream(string id)
{
ArgumentException.ThrowIfNullOrEmpty(id);
- await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
-
- try
+ using (await _liveStreamLocker.LockAsync().ConfigureAwait(false))
{
if (_openStreams.TryGetValue(id, out ILiveStream liveStream))
{
@@ -824,10 +851,6 @@ namespace Emby.Server.Implementations.Library
}
}
}
- finally
- {
- _liveStreamSemaphore.Release();
- }
}
private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
@@ -864,7 +887,7 @@ namespace Emby.Server.Implementations.Library
CloseLiveStream(key).GetAwaiter().GetResult();
}
- _liveStreamSemaphore.Dispose();
+ _liveStreamLocker.Dispose();
}
}
}
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index b2439a87e1..078f4ad219 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library
{
return Guid.Empty;
}
- }).Where(i => !i.Equals(default)).ToArray();
+ }).Where(i => !i.IsEmpty()).ToArray();
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index b916b91708..020cb517d1 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{
User user = null;
- if (!query.UserId.Equals(default))
+ if (!query.UserId.IsEmpty())
{
user = _userManager.GetUserById(query.UserId);
}
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Library
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{
- if (!searchQuery.ParentId.Equals(default))
+ if (!searchQuery.ParentId.IsEmpty())
{
searchQuery.AncestorIds = new[] { searchQuery.ParentId };
searchQuery.ParentId = Guid.Empty;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 1d662ed8dd..83a66c8e47 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -151,7 +152,7 @@ namespace Emby.Server.Implementations.Library
var index = Array.IndexOf(orders, i.Id);
if (index == -1
&& i is UserView view
- && !view.DisplayParentId.Equals(default))
+ && !view.DisplayParentId.IsEmpty())
{
index = Array.IndexOf(orders, view.DisplayParentId);
}
@@ -253,7 +254,7 @@ namespace Emby.Server.Implementations.Library
var parents = new List<BaseItem>();
- if (!parentId.Equals(default))
+ if (!parentId.IsEmpty())
{
var parentItem = _libraryManager.GetItemById(parentId);
if (parentItem is Channel)
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
deleted file mode 100644
index a2ec2df375..0000000000
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Plugins;
-
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
-{
- public sealed class EntryPoint : IServerEntryPoint
- {
- /// <inheritdoc />
- public Task RunAsync()
- {
- return EmbyTV.Current.Start();
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- }
- }
-}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
deleted file mode 100644
index 098f193fba..0000000000
--- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.LiveTv;
-
-namespace Emby.Server.Implementations.LiveTv
-{
- /// <summary>
- /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
- /// </summary>
- public class LiveTvConfigurationFactory : IConfigurationFactory
- {
- /// <inheritdoc />
- public IEnumerable<ConfigurationStore> GetConfigurations()
- {
- return new ConfigurationStore[]
- {
- new ConfigurationStore
- {
- ConfigurationType = typeof(LiveTvOptions),
- Key = "livetv"
- }
- };
- }
- }
-}
diff --git a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
deleted file mode 100644
index 72bbdd14a6..0000000000
--- a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Tasks;
-
-namespace Emby.Server.Implementations.LiveTv
-{
- /// <summary>
- /// The "Refresh Guide" scheduled task.
- /// </summary>
- public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
- {
- private readonly ILiveTvManager _liveTvManager;
- private readonly IConfigurationManager _config;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
- /// </summary>
- /// <param name="liveTvManager">The live tv manager.</param>
- /// <param name="config">The configuration manager.</param>
- public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
- {
- _liveTvManager = liveTvManager;
- _config = config;
- }
-
- /// <inheritdoc />
- public string Name => "Refresh Guide";
-
- /// <inheritdoc />
- public string Description => "Downloads channel information from live tv services.";
-
- /// <inheritdoc />
- public string Category => "Live TV";
-
- /// <inheritdoc />
- public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
-
- /// <inheritdoc />
- public string Key => "RefreshGuide";
-
- /// <inheritdoc />
- public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var manager = (LiveTvManager)_liveTvManager;
-
- return manager.RefreshChannels(progress, cancellationToken);
- }
-
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- return new[]
- {
- // Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
- };
- }
-
- private LiveTvOptions GetConfiguration()
- {
- return _config.GetConfiguration<LiveTvOptions>("livetv");
- }
- }
-}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index f1dbf3c89d..7a4c2067ba 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -121,8 +121,8 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.",
"TaskOptimizeDatabase": "Datenbank optimieren",
- "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
- "TaskKeyframeExtractor": "Keyframe Extraktor",
+ "TaskKeyframeExtractorDescription": "Extrahiert Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
+ "TaskKeyframeExtractor": "Keyframe-Extraktor",
"External": "Extern",
"HearingImpaired": "Hörgeschädigt",
"TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index 081462407d..c78ffa28c3 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -52,7 +52,7 @@
"PluginUninstalledWithName": "{0} eemaldati",
"PluginInstalledWithName": "{0} paigaldati",
"Plugin": "Plugin",
- "Playlists": "Pleilistid",
+ "Playlists": "Esitusloendid",
"Photos": "Fotod",
"NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
"NotificationOptionVideoPlayback": "Video taasesitus algas",
@@ -123,5 +123,7 @@
"External": "Väline",
"HearingImpaired": "Kuulmispuudega",
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
- "TaskKeyframeExtractor": "Võtmekaadri ekstraktor"
+ "TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
+ "TaskRefreshTrickplayImages": "Loo eelvaate pildid",
+ "TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json
index 5368b8eb6e..2d02522fea 100644
--- a/Emby.Server.Implementations/Localization/Core/ka.json
+++ b/Emby.Server.Implementations/Localization/Core/ka.json
@@ -4,9 +4,9 @@
"HeaderFavoriteAlbums": "რჩეული ალბომები",
"TasksApplicationCategory": "აპლიკაცია",
"Albums": "ალბომები",
- "AppDeviceValues": "აპი: {0}, მოწყობილობა: {1}",
+ "AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}",
"Application": "აპლიკაცია",
- "Artists": "შემსრულებლები",
+ "Artists": "არტისტი",
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
"Books": "წიგნები",
"Forced": "ძალით",
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json b/Emby.Server.Implementations/Localization/Core/ky.json
index 0967ef424b..0967ef424b 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json
+++ b/Emby.Server.Implementations/Localization/Core/ky.json
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index a07222975b..ebd3f7560b 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -124,5 +124,7 @@
"External": "Luaran",
"TaskOptimizeDatabase": "Optimumkan pangkalan data",
"TaskKeyframeExtractor": "Ekstrak bingkai kunci",
- "TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang."
+ "TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang.",
+ "TaskRefreshTrickplayImagesDescription": "Jana gambar prebiu Trickplay untuk video dalam perpustakaan.",
+ "TaskRefreshTrickplayImages": "Jana gambar Trickplay"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 0362c24179..b6c15d871a 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -5,7 +5,7 @@
"Artists": "Artister",
"AuthenticationSucceededWithUserName": "{0} har logget inn",
"Books": "Bøker",
- "CameraImageUploadedFrom": "Et nytt kamerabilde er lastet opp fra {0}",
+ "CameraImageUploadedFrom": "Et nytt kamerabilde har blitt lastet opp fra {0}",
"Channels": "Kanaler",
"ChapterNameValue": "Kapittel {0}",
"Collections": "Samlinger",
@@ -32,10 +32,10 @@
"LabelIpAddressValue": "IP-adresse: {0}",
"LabelRunningTimeValue": "Spilletid {0}",
"Latest": "Siste",
- "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert",
- "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert",
+ "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert",
+ "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjonsseksjon {0} har blitt oppdatert",
+ "MessageServerConfigurationUpdated": "Serverkonfigurasjon har blitt oppdatert",
"MixedContent": "Blandet innhold",
"Movies": "Filmer",
"Music": "Musikk",
@@ -43,19 +43,19 @@
"NameInstallFailed": "Installasjonen av {0} mislyktes",
"NameSeasonNumber": "Sesong {0}",
"NameSeasonUnknown": "Ukjent sesong",
- "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.",
+ "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
"NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
- "NotificationOptionInstallationFailed": "Installasjonen feilet",
+ "NotificationOptionInstallationFailed": "Installasjonsfeil",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
"NotificationOptionPluginError": "Programvareutvidelsesfeil",
"NotificationOptionPluginInstalled": "Programvareutvidelse installert",
"NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert",
"NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert",
- "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig",
+ "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig",
"NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave",
"NotificationOptionUserLockedOut": "Bruker er utestengt",
"NotificationOptionVideoPlayback": "Videoavspilling startet",
@@ -70,9 +70,9 @@
"ScheduledTaskFailedWithName": "{0} mislykkes",
"ScheduledTaskStartedWithName": "{0} startet",
"ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
- "Shows": "Program",
+ "Shows": "Serier",
"Songs": "Sanger",
- "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.",
+ "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
"SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}",
"Sync": "Synkroniser",
diff --git a/Emby.Server.Implementations/Localization/Core/or.json b/Emby.Server.Implementations/Localization/Core/or.json
index 0e9d81ee87..8251c12907 100644
--- a/Emby.Server.Implementations/Localization/Core/or.json
+++ b/Emby.Server.Implementations/Localization/Core/or.json
@@ -1,4 +1,12 @@
{
"External": "ବହିଃସ୍ଥ",
- "Genres": "ଧରଣ"
+ "Genres": "ଧରଣ",
+ "Albums": "ଆଲବମଗୁଡ଼ିକ",
+ "Artists": "କଳାକାରଗୁଡ଼ିକ",
+ "Application": "ଆପ୍ଲିକେସନ",
+ "Books": "ବହିଗୁଡ଼ିକ",
+ "Channels": "ଚ୍ୟାନେଲଗୁଡ଼ିକ",
+ "ChapterNameValue": "ବିଭାଗ {0}",
+ "Collections": "ସଂଗ୍ରହଗୁଡ଼ିକ",
+ "Folders": "ଫୋଲ୍ଡରଗୁଡ଼ିକ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 1944e072cb..110af11b71 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
"External": "Zunanji",
"TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
- "HearingImpaired": "Oslabljen sluh"
+ "HearingImpaired": "Oslabljen sluh",
+ "TaskRefreshTrickplayImages": "Ustvari Trickplay slike",
+ "TaskRefreshTrickplayImagesDescription": "Ustvari trickplay predoglede za posnetke v omogočenih knjižnicah."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 97062deece..1fc3cdbaaa 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -43,7 +43,7 @@
"NameInstallFailed": "{0} installationen misslyckades",
"NameSeasonNumber": "Säsong {0}",
"NameSeasonUnknown": "Okänd säsong",
- "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig att hämta.",
+ "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig för nedladdning.",
"NotificationOptionApplicationUpdateAvailable": "Ny programversion tillgänglig",
"NotificationOptionApplicationUpdateInstalled": "Programuppdatering installerad",
"NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats",
@@ -74,7 +74,7 @@
"Songs": "Låtar",
"StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.",
"SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades",
- "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}",
+ "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}",
"Sync": "Synk",
"System": "System",
"TvShows": "TV-serier",
diff --git a/Emby.Server.Implementations/Localization/Core/ur.json b/Emby.Server.Implementations/Localization/Core/ur.json
new file mode 100644
index 0000000000..3766830413
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ur.json
@@ -0,0 +1,3 @@
+{
+ "Books": "کتابیں"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 44ce4ac5b2..e92752c5f7 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractor": "Trích Xuất Khung Hình",
"TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.",
"External": "Bên ngoài",
- "HearingImpaired": "Khiếm Thính"
+ "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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index e8b8c2c5fa..3ab9774c27 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -1,15 +1,15 @@
{
"Albums": "專輯",
- "AppDeviceValues": "程式: {0}, 設備: {1}",
+ "AppDeviceValues": "程式:{0},設備:{1}",
"Application": "應用程式",
"Artists": "藝人",
- "AuthenticationSucceededWithUserName": "{0} 授權成功",
+ "AuthenticationSucceededWithUserName": "成功授權 {0}",
"Books": "書籍",
"CameraImageUploadedFrom": "{0} 成功上傳一張新照片",
"Channels": "頻道",
"ChapterNameValue": "第 {0} 章",
"Collections": "系列",
- "DeviceOfflineWithName": "{0} 已斷開連接",
+ "DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "{0} 登入失敗",
"Favorites": "我的最愛",
@@ -27,15 +27,15 @@
"HeaderRecordingGroups": "錄製組",
"HomeVideos": "家庭影片",
"Inherit": "繼承",
- "ItemAddedWithName": "{0} 已被添加至媒體庫",
+ "ItemAddedWithName": "{0} 已被加入至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
- "LabelIpAddressValue": "IP 地址: {0}",
- "LabelRunningTimeValue": "運行時間: {0}",
+ "LabelIpAddressValue": "IP 地址:{0}",
+ "LabelRunningTimeValue": "運作時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 已被更新",
"MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新",
- "MessageServerConfigurationUpdated": "伺服器設定已經被更新",
+ "MessageServerConfigurationUpdated": "已更新伺服器設定",
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
@@ -43,23 +43,23 @@
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知的季度",
- "NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。",
+ "NewVersionIsAvailable": "有新版本的 Jellyfin 可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的更新",
- "NotificationOptionApplicationUpdateInstalled": "應用程式已被更新",
- "NotificationOptionAudioPlayback": "開始播放音訊",
+ "NotificationOptionApplicationUpdateInstalled": "完成更新應用程式",
+ "NotificationOptionAudioPlayback": "播放音訊",
"NotificationOptionAudioPlaybackStopped": "停止播放音訊",
- "NotificationOptionCameraImageUploaded": "相片已被上傳",
+ "NotificationOptionCameraImageUploaded": "相片上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
- "NotificationOptionNewLibraryContent": "已添加新内容",
- "NotificationOptionPluginError": "插件出現錯誤",
- "NotificationOptionPluginInstalled": "插件已被安裝",
- "NotificationOptionPluginUninstalled": "插件已被移除",
- "NotificationOptionPluginUpdateInstalled": "插件已被更新",
+ "NotificationOptionNewLibraryContent": "新增媒體",
+ "NotificationOptionPluginError": "插件錯誤",
+ "NotificationOptionPluginInstalled": "安裝插件",
+ "NotificationOptionPluginUninstalled": "解除安裝插件",
+ "NotificationOptionPluginUpdateInstalled": "完成更新插件",
"NotificationOptionServerRestartRequired": "伺服器需要重啟",
- "NotificationOptionTaskFailed": "排程任務執行失敗",
- "NotificationOptionUserLockedOut": "用戶已被鎖定",
- "NotificationOptionVideoPlayback": "開始播放影片",
- "NotificationOptionVideoPlaybackStopped": "已停止播放影片",
+ "NotificationOptionTaskFailed": "排程工作執行失敗",
+ "NotificationOptionUserLockedOut": "封鎖用戶",
+ "NotificationOptionVideoPlayback": "播放影片",
+ "NotificationOptionVideoPlaybackStopped": "停止播放影片",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "插件",
@@ -68,7 +68,7 @@
"PluginUpdatedWithName": "已更新 {0}",
"ProviderValue": "提供者:{0}",
"ScheduledTaskFailedWithName": "{0} 執行失敗",
- "ScheduledTaskStartedWithName": "{0} 開始執行",
+ "ScheduledTaskStartedWithName": "開始執行 {0}",
"ServerNameNeedsToBeRestarted": "{0} 需要重啟",
"Shows": "節目",
"Songs": "歌曲",
@@ -79,50 +79,52 @@
"System": "系統",
"TvShows": "電視節目",
"User": "用戶",
- "UserCreatedWithName": "用戶 {0} 已被建立",
+ "UserCreatedWithName": "建立新用戶 {0}",
"UserDeletedWithName": "用戶 {0} 已被移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
- "UserLockedOutWithName": "使用者 {0} 已被鎖定",
- "UserOfflineFromDevice": "{0} 從 {1} 斷開連接",
+ "UserLockedOutWithName": "用戶 {0} 已被封鎖",
+ "UserOfflineFromDevice": "{0} 終止了 {1} 的連接",
"UserOnlineFromDevice": "{0} 從 {1} 連線",
- "UserPasswordChangedWithName": "{0} 的密碼已被變改",
- "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
- "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
- "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 上播放 {1}",
- "ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫",
+ "UserPasswordChangedWithName": "{0} 的密碼已被更改",
+ "UserPolicyUpdatedWithName": "使用條款已更新為 {0}",
+ "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}",
+ "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}",
+ "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本 {0}",
- "TaskDownloadMissingSubtitles": "下載缺少的字幕",
+ "TaskDownloadMissingSubtitles": "下載欠缺字幕",
"TaskUpdatePlugins": "更新插件",
"TasksApplicationCategory": "應用程式",
- "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增檔案及重新載入 metadata。",
+ "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增的檔案及重新載入元數據。",
"TasksMaintenanceCategory": "維護",
- "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在互聯網上搜索缺少的字幕。",
+ "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在網上搜尋欠缺的字幕。",
"TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。",
"TaskRefreshChannels": "重新載入頻道",
- "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。",
- "TaskCleanTranscode": "清理轉碼目錄",
+ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
+ "TaskCleanTranscode": "清理轉碼檔資料夾",
"TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。",
- "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。",
+ "TaskRefreshPeopleDescription": "更新你的媒體中有關的演員和導演的元數據。",
"TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。",
- "TaskCleanLogs": "清理紀錄檔目錄",
+ "TaskCleanLogs": "清理紀錄檔資料夾",
"TaskRefreshLibrary": "掃描媒體庫",
"TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。",
"TaskRefreshChapterImages": "提取章節圖像",
"TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。",
- "TaskCleanCache": "清理緩存目錄",
+ "TaskCleanCache": "清理緩存資料夾",
"TasksChannelsCategory": "網絡頻道",
- "TasksLibraryCategory": "庫",
+ "TasksLibraryCategory": "媒體庫",
"TaskRefreshPeople": "重新載入人物",
"TaskCleanActivityLog": "清理活動記錄",
"Undefined": "未定義",
"Forced": "強制",
"Default": "預設",
- "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
+ "TaskOptimizeDatabaseDescription": "壓縮數據庫及釋放可用空間。完成任何會修改數據庫的工作(例如掃描媒體庫)後,執行此工作或可提升伺服器速度。",
"TaskOptimizeDatabase": "最佳化數據庫",
"TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。",
- "TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。",
- "TaskKeyframeExtractor": "關鍵幀提取器",
+ "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)以建立更準確的 HLS playlist。此工作可能需要使用較長時間來完成。",
+ "TaskKeyframeExtractor": "關鍵影格提取器",
"External": "外部",
- "HearingImpaired": "聽力障礙"
+ "HearingImpaired": "聽力障礙",
+ "TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
+ "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。"
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index d2e2fd7d56..aea8d65322 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -178,7 +179,7 @@ namespace Emby.Server.Implementations.Playlists
public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{
- var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+ var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 1af2c96d2f..efb6436ae9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -14,7 +14,6 @@ using Jellyfin.Data.Events;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -371,7 +370,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
throw new InvalidOperationException("Cannot execute a Task that is already running");
}
- var progress = new SimpleProgress<double>();
+ var progress = new Progress<double>();
CurrentCancellationTokenSource = new CancellationTokenSource();
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index acd4bf9056..812df81922 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -115,9 +115,10 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
List<LinkedChild>? itemsToRemove = null;
foreach (var linkedChild in folder.LinkedChildren)
{
- if (!File.Exists(folder.Path))
+ var path = linkedChild.Path;
+ if (!File.Exists(path))
{
- _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path);
+ _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
(itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
}
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 6f599e4c7c..bbb3938dcf 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.Session
_logger);
}
- private void OnSessionEnded(SessionInfo info)
+ private async ValueTask OnSessionEnded(SessionInfo info)
{
EventHelper.QueueEventIfNotNull(
SessionEnded,
@@ -202,7 +202,7 @@ namespace Emby.Server.Implementations.Session
_eventManager.Publish(new SessionEndedEventArgs(info));
- info.Dispose();
+ await info.DisposeAsync().ConfigureAwait(false);
}
/// <inheritdoc />
@@ -301,12 +301,12 @@ namespace Emby.Server.Implementations.Session
await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
}
- OnSessionEnded(session);
+ await OnSessionEnded(session).ConfigureAwait(false);
}
}
/// <inheritdoc />
- public void ReportSessionEnded(string sessionId)
+ public async ValueTask ReportSessionEnded(string sessionId)
{
CheckDisposed();
var session = GetSession(sessionId, false);
@@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Session
_activeConnections.TryRemove(key, out _);
- OnSessionEnded(session);
+ await OnSessionEnded(session).ConfigureAwait(false);
}
}
@@ -337,7 +337,7 @@ namespace Emby.Server.Implementations.Session
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
}
- if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null)
+ if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
{
var current = session.NowPlayingItem;
@@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Session
{
var users = new List<User>();
- if (session.UserId.Equals(default))
+ if (session.UserId.IsEmpty())
{
return users;
}
@@ -690,7 +690,7 @@ namespace Emby.Server.Implementations.Session
var session = GetSession(info.SessionId);
- var libraryItem = info.ItemId.Equals(default)
+ var libraryItem = info.ItemId.IsEmpty()
? null
: GetNowPlayingItem(session, info.ItemId);
@@ -784,7 +784,7 @@ namespace Emby.Server.Implementations.Session
var session = GetSession(info.SessionId);
- var libraryItem = info.ItemId.Equals(default)
+ var libraryItem = info.ItemId.IsEmpty()
? null
: GetNowPlayingItem(session, info.ItemId);
@@ -923,7 +923,7 @@ namespace Emby.Server.Implementations.Session
session.StopAutomaticProgress();
- var libraryItem = info.ItemId.Equals(default)
+ var libraryItem = info.ItemId.IsEmpty()
? null
: GetNowPlayingItem(session, info.ItemId);
@@ -933,7 +933,7 @@ namespace Emby.Server.Implementations.Session
info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
}
- if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null)
+ if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
{
var current = session.NowPlayingItem;
@@ -1154,7 +1154,7 @@ namespace Emby.Server.Implementations.Session
var session = GetSessionToRemoteControl(sessionId);
- var user = session.UserId.Equals(default) ? null : _userManager.GetUserById(session.UserId);
+ var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
List<BaseItem> items;
@@ -1223,7 +1223,7 @@ namespace Emby.Server.Implementations.Session
{
var controllingSession = GetSession(controllingSessionId);
AssertCanControl(session, controllingSession);
- if (!controllingSession.UserId.Equals(default))
+ if (!controllingSession.UserId.IsEmpty())
{
command.ControllingUserId = controllingSession.UserId;
}
@@ -1342,7 +1342,7 @@ namespace Emby.Server.Implementations.Session
{
var controllingSession = GetSession(controllingSessionId);
AssertCanControl(session, controllingSession);
- if (!controllingSession.UserId.Equals(default))
+ if (!controllingSession.UserId.IsEmpty())
{
command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
}
@@ -1463,7 +1463,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
User user = null;
- if (!request.UserId.Equals(default))
+ if (!request.UserId.IsEmpty())
{
user = _userManager.GetUserById(request.UserId);
}
@@ -1590,7 +1590,7 @@ namespace Emby.Server.Implementations.Session
{
try
{
- ReportSessionEnded(session.Id);
+ await ReportSessionEnded(session.Id).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -1766,7 +1766,7 @@ namespace Emby.Server.Implementations.Session
{
ArgumentNullException.ThrowIfNull(info);
- var user = info.UserId.Equals(default)
+ var user = info.UserId.IsEmpty()
? null
: _userManager.GetUserById(info.UserId);
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index da8f949326..a7821c0e0e 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
@@ -553,7 +554,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (playingItemRemoved)
{
var itemId = PlayQueue.GetPlayingItemId();
- if (!itemId.Equals(default))
+ if (!itemId.IsEmpty())
{
var item = _libraryManager.GetItemById(itemId);
RunTimeTicks = item.RunTimeTicks ?? 0;
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index ef890aeb4f..34c9e86f26 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -41,7 +42,7 @@ namespace Emby.Server.Implementations.TV
}
string? presentationUniqueKey = null;
- if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
+ if (!query.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
{
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.TV
string? presentationUniqueKey = null;
int? limit = null;
- if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
+ if (!request.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
{
@@ -146,7 +147,7 @@ namespace Emby.Server.Implementations.TV
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
- var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default);
+ var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
var anyFound = false;
return allNextUp
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 15c4cfdf02..ce3d6cab88 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.Updates
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
- if (!id.Equals(default))
+ if (!id.IsEmpty())
{
availablePackages = availablePackages.Where(x => x.Id.Equals(id));
}
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
index cf3cb69052..7d0fe55898 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -2,6 +2,7 @@
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
@@ -41,7 +42,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
var isApiKey = context.User.GetIsApiKey();
var userId = context.User.GetUserId();
// This likely only happens during the wizard, so skip the default checks and let any other handlers do it
- if (!isApiKey && userId.Equals(default))
+ if (!isApiKey && userId.IsEmpty())
{
return Task.CompletedTask;
}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 688a13bc0b..965b7e7e60 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,6 +1,7 @@
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;
@@ -46,7 +47,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
}
var userId = contextUser.GetUserId();
- if (userId.Equals(default))
+ if (userId.IsEmpty())
{
context.Fail();
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index e7d3e694ab..8b931f1621 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -126,7 +127,7 @@ public class ArtistsController : BaseJellyfinApiController
User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- if (!userId.Value.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
user = _userManager.GetUserById(userId.Value);
}
@@ -330,7 +331,7 @@ public class ArtistsController : BaseJellyfinApiController
User? user = null;
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- if (!userId.Value.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
user = _userManager.GetUserById(userId.Value);
}
@@ -469,7 +470,7 @@ public class ArtistsController : BaseJellyfinApiController
var item = _libraryManager.GetArtist(name, dtoOptions);
- if (!userId.Value.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
var user = _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index fdc16ee23f..f83c71b578 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -126,7 +127,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -201,7 +202,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index dda1e9d561..590cdc33f0 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -294,9 +294,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (!System.IO.File.Exists(playlistPath))
{
- var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
{
if (!System.IO.File.Exists(playlistPath))
{
@@ -326,10 +324,6 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
}
- finally
- {
- transcodingLock.Release();
- }
}
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
@@ -1442,95 +1436,80 @@ public class DynamicHlsController : BaseJellyfinApiController
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- var released = false;
- var startTranscoding = false;
-
- try
+ using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
{
+ var startTranscoding = false;
if (System.IO.File.Exists(segmentPath))
{
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- transcodingLock.Release();
- released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- else
- {
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
- if (segmentId == -1)
- {
- _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
- startTranscoding = true;
- segmentId = 0;
- }
- else if (currentTranscodingIndex is null)
- {
- _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
- startTranscoding = true;
- }
- else if (segmentId < currentTranscodingIndex.Value)
- {
- _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
- startTranscoding = true;
- }
- else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
- {
- _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
- startTranscoding = true;
- }
- if (startTranscoding)
- {
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
- .ConfigureAwait(false);
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
- if (currentTranscodingIndex.HasValue)
- {
- DeleteLastFile(playlistPath, segmentExtension, 0);
- }
+ if (segmentId == -1)
+ {
+ _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
+ startTranscoding = true;
+ segmentId = 0;
+ }
+ else if (currentTranscodingIndex is null)
+ {
+ _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
+ startTranscoding = true;
+ }
+ else if (segmentId < currentTranscodingIndex.Value)
+ {
+ _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
+ startTranscoding = true;
+ }
+ else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
+ {
+ _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
+ startTranscoding = true;
+ }
- streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+ if (startTranscoding)
+ {
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
+ {
+ await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
+ .ConfigureAwait(false);
- state.WaitForPath = segmentPath;
- job = await _transcodeManager.StartFfMpeg(
- state,
- playlistPath,
- GetCommandLineArguments(playlistPath, state, false, segmentId),
- Request.HttpContext.User.GetUserId(),
- TranscodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
- }
- catch
+ if (currentTranscodingIndex.HasValue)
{
- state.Dispose();
- throw;
+ DeleteLastFile(playlistPath, segmentExtension, 0);
}
- // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+ streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+
+ state.WaitForPath = segmentPath;
+ job = await _transcodeManager.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state, false, segmentId),
+ Request.HttpContext.User.GetUserId(),
+ TranscodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
}
- else
+ catch
{
- job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- if (job?.TranscodingThrottler is not null)
- {
- await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
- }
+ state.Dispose();
+ throw;
}
+
+ // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
}
- }
- finally
- {
- if (!released)
+ else
{
- transcodingLock.Release();
+ job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ if (job?.TranscodingThrottler is not null)
+ {
+ await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index baeb8b81a0..d6e043e6a1 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -3,6 +3,7 @@ using System.Linq;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -53,7 +54,7 @@ public class FilterController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -146,7 +147,7 @@ public class FilterController : BaseJellyfinApiController
[FromQuery] bool? recursive)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 6cb1993e46..54d48aec21 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -95,7 +96,7 @@ public class GenresController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId.Value.Equals(default)
+ User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -172,7 +173,7 @@ public class GenresController : BaseJellyfinApiController
item ??= new Genre();
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 4dc2a4253d..e7ff1f9868 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -76,7 +77,7 @@ public class InstantMixController : BaseJellyfinApiController
{
var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -113,7 +114,7 @@ public class InstantMixController : BaseJellyfinApiController
{
var album = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -150,7 +151,7 @@ public class InstantMixController : BaseJellyfinApiController
{
var playlist = (Playlist)_libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -186,7 +187,7 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -223,7 +224,7 @@ public class InstantMixController : BaseJellyfinApiController
{
var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -260,7 +261,7 @@ public class InstantMixController : BaseJellyfinApiController
{
var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
@@ -334,7 +335,7 @@ public class InstantMixController : BaseJellyfinApiController
{
var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index a1fc8e11b2..d10fba920c 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -245,7 +246,7 @@ public class ItemsController : BaseJellyfinApiController
var isApiKey = User.GetIsApiKey();
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
userId = RequestHelpers.GetUserId(User, userId);
- var user = !isApiKey && !userId.Value.Equals(default)
+ var user = !isApiKey && !userId.IsNullOrEmpty()
? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
: null;
@@ -840,7 +841,7 @@ public class ItemsController : BaseJellyfinApiController
var ancestorIds = Array.Empty<Guid>();
var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
- if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0)
+ if (parentIdGuid.IsEmpty() && excludeFolderIds.Length > 0)
{
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index de057bbab9..e357588d1d 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -7,7 +7,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -17,7 +16,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -146,12 +144,12 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] bool inheritFromParent = false)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = itemId.Equals(default)
- ? (userId.Value.Equals(default)
+ var item = itemId.IsEmpty()
+ ? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
@@ -213,12 +211,12 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] bool inheritFromParent = false)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = itemId.Equals(default)
- ? (userId.Value.Equals(default)
+ var item = itemId.IsEmpty()
+ ? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
@@ -313,7 +311,7 @@ public class LibraryController : BaseJellyfinApiController
{
try
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -339,7 +337,7 @@ public class LibraryController : BaseJellyfinApiController
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
- var user = !isApiKey && !userId.Equals(default)
+ var user = !isApiKey && !userId.IsEmpty()
? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
: null;
if (!isApiKey && user is null)
@@ -382,7 +380,7 @@ public class LibraryController : BaseJellyfinApiController
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
- var user = !isApiKey && !userId.Equals(default)
+ var user = !isApiKey && !userId.IsEmpty()
? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
: null;
@@ -428,7 +426,7 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery] bool? isFavorite)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -471,7 +469,7 @@ public class LibraryController : BaseJellyfinApiController
var baseItemDtos = new List<BaseItemDto>();
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -702,8 +700,8 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
- var item = itemId.Equals(default)
- ? (userId.Value.Equals(default)
+ var item = itemId.IsEmpty()
+ ? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
@@ -718,7 +716,7 @@ public class LibraryController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>();
}
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index d483ca4d2b..23c430f859 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -6,11 +6,9 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Api;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -180,7 +178,7 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
else
{
@@ -224,7 +222,7 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
else
{
@@ -293,7 +291,7 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
else
{
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 5502836239..da68c72c99 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -10,12 +10,12 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -43,6 +43,8 @@ namespace Jellyfin.Api.Controllers;
public class LiveTvController : BaseJellyfinApiController
{
private readonly ILiveTvManager _liveTvManager;
+ private readonly IGuideManager _guideManager;
+ private readonly ITunerHostManager _tunerHostManager;
private readonly IUserManager _userManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
@@ -55,6 +57,8 @@ public class LiveTvController : BaseJellyfinApiController
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
/// </summary>
/// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
+ /// <param name="guideManager">Instance of the <see cref="IGuideManager"/> interface.</param>
+ /// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
@@ -64,6 +68,8 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
+ IGuideManager guideManager,
+ ITunerHostManager tunerHostManager,
IUserManager userManager,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
@@ -73,6 +79,8 @@ public class LiveTvController : BaseJellyfinApiController
ITranscodeManager transcodeManager)
{
_liveTvManager = liveTvManager;
+ _guideManager = guideManager;
+ _tunerHostManager = tunerHostManager;
_userManager = userManager;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
@@ -179,7 +187,7 @@ public class LiveTvController : BaseJellyfinApiController
dtoOptions,
CancellationToken.None);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -211,10 +219,10 @@ public class LiveTvController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = channelId.Equals(default)
+ var item = channelId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(channelId);
@@ -384,7 +392,7 @@ public class LiveTvController : BaseJellyfinApiController
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false);
@@ -407,10 +415,10 @@ public class LiveTvController : BaseJellyfinApiController
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+ var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -564,7 +572,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool enableTotalRecordCount = true)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -591,7 +599,7 @@ public class LiveTvController : BaseJellyfinApiController
GenreIds = genreIds
};
- if (librarySeriesId.HasValue && !librarySeriesId.Equals(default))
+ if (!librarySeriesId.IsNullOrEmpty())
{
query.IsSeries = true;
@@ -620,7 +628,7 @@ public class LiveTvController : BaseJellyfinApiController
[Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
{
- var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
+ var user = body.UserId.IsEmpty() ? null : _userManager.GetUserById(body.UserId);
var query = new InternalItemsQuery(user)
{
@@ -645,7 +653,7 @@ public class LiveTvController : BaseJellyfinApiController
GenreIds = body.GenreIds
};
- if (!body.LibrarySeriesId.Equals(default))
+ if (!body.LibrarySeriesId.IsEmpty())
{
query.IsSeries = true;
@@ -704,7 +712,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool enableTotalRecordCount = true)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -743,7 +751,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -937,9 +945,7 @@ public class LiveTvController : BaseJellyfinApiController
[Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<GuideInfo> GetGuideInfo()
- {
- return _liveTvManager.GetGuideInfo();
- }
+ => _guideManager.GetGuideInfo();
/// <summary>
/// Adds a tuner host.
@@ -951,9 +957,7 @@ public class LiveTvController : BaseJellyfinApiController
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
- {
- return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
- }
+ => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
/// <summary>
/// Deletes a tuner host.
@@ -1130,10 +1134,8 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("TunerHosts/Types")]
[Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
- {
- return _liveTvManager.GetTunerHostTypes();
- }
+ public IEnumerable<NameIdPair> GetTunerHostTypes()
+ => _tunerHostManager.GetTunerHostTypes();
/// <summary>
/// Discover tuners.
@@ -1145,10 +1147,8 @@ public class LiveTvController : BaseJellyfinApiController
[HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.LiveTvManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
- {
- return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
- }
+ public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
+ => _tunerHostManager.DiscoverTuners(newDevicesOnly);
/// <summary>
/// Gets a live tv recording stream.
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index e1145481fa..471bcd096e 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -7,6 +7,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -69,7 +70,7 @@ public class MoviesController : BaseJellyfinApiController
[FromQuery] int itemLimit = 8)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 69b9042646..5411baa3e7 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -95,7 +96,7 @@ public class MusicGenresController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId.Value.Equals(default)
+ User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -164,7 +165,7 @@ public class MusicGenresController : BaseJellyfinApiController
return NotFound();
}
- if (!userId.Value.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
var user = _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index b4c6f490a0..6ca3086015 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -83,7 +84,7 @@ public class PersonsController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId.Value.Equals(default)
+ User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -129,7 +130,7 @@ public class PersonsController : BaseJellyfinApiController
return NotFound();
}
- if (!userId.Value.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
var user = _userManager.GetUserById(userId.Value);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index c4c89ccde0..921cc6031f 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -9,6 +9,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -188,7 +189,7 @@ public class PlaylistsController : BaseJellyfinApiController
return NotFound();
}
- var user = userId.Equals(default)
+ var user = userId.IsEmpty()
? null
: _userManager.GetUserById(userId);
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 5b45941657..413b7b8340 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -209,7 +209,7 @@ public class SearchController : BaseJellyfinApiController
break;
}
- if (!item.ChannelId.Equals(default))
+ if (!item.ChannelId.IsEmpty())
{
var channel = _libraryManager.GetItemById(item.ChannelId);
result.ChannelName = channel?.Name;
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 083515a949..52b58b8f10 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.SessionDtos;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@@ -71,7 +72,7 @@ public class SessionController : BaseJellyfinApiController
result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
}
- if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
+ if (!controllableByUserId.IsNullOrEmpty())
{
result = result.Where(i => i.SupportsRemoteControl);
@@ -83,12 +84,12 @@ public class SessionController : BaseJellyfinApiController
if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
- result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value));
+ result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableByUserId.Value));
}
if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
{
- result = result.Where(i => !i.UserId.Equals(default));
+ result = result.Where(i => !i.UserId.IsEmpty());
}
result = result.Where(i =>
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index f434f60f51..708fc7436f 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -5,6 +5,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -91,7 +92,7 @@ public class StudiosController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId.Value.Equals(default)
+ User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -144,7 +145,7 @@ public class StudiosController : BaseJellyfinApiController
var dtoOptions = new DtoOptions().AddClientFields(User);
var item = _libraryManager.GetStudio(name);
- if (!userId.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
var user = _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 675757fc51..2aa6d25a79 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -62,7 +63,7 @@ public class SuggestionsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
{
- var user = userId.Equals(default)
+ var user = userId.IsEmpty()
? null
: _userManager.GetUserById(userId);
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 3d4df03869..6c5ce47158 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -188,16 +188,24 @@ public class SystemController : BaseJellyfinApiController
/// <param name="name">The name of the log file to get.</param>
/// <response code="200">Log file retrieved.</response>
/// <response code="403">User does not have permission to get log files.</response>
+ /// <response code="404">Could not find a log file with the name.</response>
/// <returns>The log file.</returns>
[HttpGet("Logs/Log")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile(MediaTypeNames.Text.Plain)]
public ActionResult GetLogFile([FromQuery, Required] string name)
{
- var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
- .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ var file = _fileSystem
+ .GetFiles(_appPaths.LogDirectoryPath)
+ .FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+
+ if (file is null)
+ {
+ return NotFound("Log file not found.");
+ }
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 55a30d4692..3d84b61bf4 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -111,7 +111,7 @@ public class TvShowsController : BaseJellyfinApiController
},
options);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -150,7 +150,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -222,7 +222,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] ItemSortBy? sortBy)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
@@ -284,7 +284,7 @@ public class TvShowsController : BaseJellyfinApiController
}
// This must be the last filter
- if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
+ if (!adjacentTo.IsNullOrEmpty())
{
episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
@@ -339,7 +339,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] bool? enableUserData)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index f9f27f1480..ea10ee24f9 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -532,7 +533,7 @@ public class UserController : BaseJellyfinApiController
public ActionResult<UserDto> GetCurrentUser()
{
var userId = User.GetUserId();
- if (userId.Equals(default))
+ if (userId.IsEmpty())
{
return BadRequest();
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 2c4fe91862..264e0a3dbd 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -84,7 +85,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -145,7 +146,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -185,7 +186,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -221,7 +222,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -257,7 +258,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -294,7 +295,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -330,7 +331,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -375,7 +376,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
@@ -558,7 +559,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- var item = itemId.Equals(default)
+ var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index c231c147fc..e6c3198698 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -11,6 +11,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -96,12 +97,12 @@ public class VideosController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- var user = userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = itemId.Equals(default)
- ? (userId.Value.Equals(default)
+ var item = itemId.IsEmpty()
+ ? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
: _libraryManager.GetItemById(itemId);
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index ca46c38c59..e4aa0ea42d 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -90,7 +90,7 @@ public class YearsController : BaseJellyfinApiController
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId.Value.Equals(default)
+ User? user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
@@ -110,7 +110,7 @@ public class YearsController : BaseJellyfinApiController
{
var folder = (Folder)parentItem;
- if (userId.Equals(default))
+ if (userId.IsNullOrEmpty())
{
items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
}
@@ -182,7 +182,7 @@ public class YearsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions()
.AddClientFields(User);
- if (!userId.Value.Equals(default))
+ if (!userId.IsNullOrEmpty())
{
var user = _userManager.GetUserById(userId.Value);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index fa81fc284d..b0c17c8356 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -325,6 +325,7 @@ public class DynamicHlsHelper
if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
{
var videoRange = state.VideoStream.VideoRange;
+ var videoRangeType = state.VideoStream.VideoRangeType;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
if (videoRange == VideoRange.SDR)
@@ -334,7 +335,14 @@ public class DynamicHlsHelper
if (videoRange == VideoRange.HDR)
{
- builder.Append(",VIDEO-RANGE=PQ");
+ if (videoRangeType == VideoRangeType.HLG)
+ {
+ builder.Append(",VIDEO-RANGE=HLG");
+ }
+ else
+ {
+ builder.Append(",VIDEO-RANGE=PQ");
+ }
}
}
else
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 5385979d4a..cb178a61d8 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -93,9 +93,7 @@ public static class FileStreamResponseHelpers
return new OkResult();
}
- var transcodingLock = transcodeManager.GetTranscodingLock(outputPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
- try
+ using (await transcodeManager.LockAsync(outputPath, cancellationTokenSource.Token).ConfigureAwait(false))
{
TranscodingJob? job;
if (!File.Exists(outputPath))
@@ -117,9 +115,5 @@ public static class FileStreamResponseHelpers
var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
return new FileStreamResult(stream, contentType);
}
- finally
- {
- transcodingLock.Release();
- }
}
}
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 321987ca74..6a24ad32ab 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -86,7 +87,7 @@ public class MediaInfoHelper
string? mediaSourceId = null,
string? liveStreamId = null)
{
- var user = userId is null || userId.Value.Equals(default)
+ var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var item = _libraryManager.GetItemById(id);
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index be3d4dfb67..429e972132 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -7,6 +7,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -67,7 +68,7 @@ public static class RequestHelpers
var authenticatedUserId = claimsPrincipal.GetUserId();
// UserId not provided, fall back to authenticated user id.
- if (userId is null || userId.Value.Equals(default))
+ if (userId.IsNullOrEmpty())
{
return authenticatedUserId;
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 78943f7b58..7a3842a9f7 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -82,7 +82,7 @@ public static class StreamingHelpers
};
var userId = httpContext.User.GetUserId();
- if (!userId.Equals(default))
+ if (!userId.IsEmpty())
{
state.User = userManager.GetUserById(userId);
}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index acd3f29e34..12ce19368b 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
@@ -50,6 +51,18 @@ public class ClientCapabilitiesDto
/// </summary>
public string? IconUrl { get; set; }
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ // TODO: Remove after 10.9
+ [Obsolete("Unused")]
+ [DefaultValue(false)]
+ public bool? SupportsContentUploading { get; set; }
+
+ // TODO: Remove after 10.9
+ [Obsolete("Unused")]
+ [DefaultValue(false)]
+ public bool? SupportsSync { get; set; }
+#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+
/// <summary>
/// Convert the dto to the full <see cref="ClientCapabilities"/> model.
/// </summary>
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 0ed1578c70..7c4155bfc6 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,6 +26,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index b960feb7f3..f6854157a6 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -37,7 +38,7 @@ public class TrickplayManager : ITrickplayManager
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IApplicationPaths _appPaths;
- private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+ private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
/// <summary>
@@ -107,93 +108,92 @@ public class TrickplayManager : ITrickplayManager
var imgTempDir = string.Empty;
var outputDir = GetTrickplayDirectory(video, width);
- await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
- if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
- {
- _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
- return;
- }
-
- // Extract images
- // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
- var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
-
- if (mediaSource is null)
+ try
{
- _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
- return;
- }
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
+ return;
+ }
- var mediaPath = mediaSource.Path;
- var mediaStream = mediaSource.VideoStream;
- var container = mediaSource.Container;
+ // Extract images
+ // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+ var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
- _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
- imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
- mediaPath,
- container,
- mediaSource,
- mediaStream,
- width,
- TimeSpan.FromMilliseconds(options.Interval),
- options.EnableHwAcceleration,
- options.ProcessThreads,
- options.Qscale,
- options.ProcessPriority,
- _encodingHelper,
- cancellationToken).ConfigureAwait(false);
+ if (mediaSource is null)
+ {
+ _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+ return;
+ }
- if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
- {
- throw new InvalidOperationException("Null or invalid directory from media encoder.");
- }
+ var mediaPath = mediaSource.Path;
+ var mediaStream = mediaSource.VideoStream;
+ var container = mediaSource.Container;
+
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+ mediaPath,
+ container,
+ mediaSource,
+ mediaStream,
+ width,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
+ _encodingHelper,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+ {
+ throw new InvalidOperationException("Null or invalid directory from media encoder.");
+ }
- var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
- .Select(i => i.FullName)
- .OrderBy(i => i)
- .ToList();
+ var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
+ .ToList();
- // Create tiles
- var trickplayInfo = CreateTiles(images, width, options, outputDir);
+ // Create tiles
+ var trickplayInfo = CreateTiles(images, width, options, outputDir);
- // Save tiles info
- try
- {
- if (trickplayInfo is not null)
+ // Save tiles info
+ try
{
- trickplayInfo.ItemId = video.Id;
- await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+ if (trickplayInfo is not null)
+ {
+ trickplayInfo.ItemId = video.Id;
+ await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
- _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ }
}
- else
+ catch (Exception ex)
{
- throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ _logger.LogError(ex, "Error while saving trickplay tiles info.");
+
+ // Make sure no files stay in metadata folders on failure
+ // if tiles info wasn't saved.
+ Directory.Delete(outputDir, true);
}
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error while saving trickplay tiles info.");
-
- // Make sure no files stay in metadata folders on failure
- // if tiles info wasn't saved.
- Directory.Delete(outputDir, true);
+ _logger.LogError(ex, "Error creating trickplay images.");
}
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating trickplay images.");
- }
- finally
- {
- _resourcePool.Release();
-
- if (!string.IsNullOrEmpty(imgTempDir))
+ finally
{
- Directory.Delete(imgTempDir, true);
+ if (!string.IsNullOrEmpty(imgTempDir))
+ {
+ Directory.Delete(imgTempDir, true);
+ }
}
}
}
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
deleted file mode 100644
index a471ea1d50..0000000000
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Queries;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-
-namespace Jellyfin.Server.Implementations.Users
-{
- public sealed class DeviceAccessEntryPoint : IServerEntryPoint
- {
- private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
- private readonly ISessionManager _sessionManager;
-
- public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
- {
- _userManager = userManager;
- _deviceManager = deviceManager;
- _sessionManager = sessionManager;
- }
-
- public Task RunAsync()
- {
- _userManager.OnUserUpdated += OnUserUpdated;
-
- return Task.CompletedTask;
- }
-
- public void Dispose()
- {
- }
-
- private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
- {
- var user = e.Argument;
- if (!user.HasPermission(PermissionKind.EnableAllDevices))
- {
- await UpdateDeviceAccess(user).ConfigureAwait(false);
- }
- }
-
- private async Task UpdateDeviceAccess(User user)
- {
- var existing = (await _deviceManager.GetDevices(new DeviceQuery
- {
- UserId = user.Id
- }).ConfigureAwait(false)).Items;
-
- foreach (var device in existing)
- {
- if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
- {
- await _sessionManager.Logout(device).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
new file mode 100644
index 0000000000..e40b541a35
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
@@ -0,0 +1,76 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using Microsoft.Extensions.Hosting;
+
+namespace Jellyfin.Server.Implementations.Users;
+
+/// <summary>
+/// <see cref="IHostedService"/> responsible for managing user device permissions.
+/// </summary>
+public sealed class DeviceAccessHost : IHostedService
+{
+ private readonly IUserManager _userManager;
+ private readonly IDeviceManager _deviceManager;
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceAccessHost"/> class.
+ /// </summary>
+ /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+ /// <param name="deviceManager">The <see cref="IDeviceManager"/>.</param>
+ /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+ public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
+ {
+ _userManager = userManager;
+ _deviceManager = deviceManager;
+ _sessionManager = sessionManager;
+ }
+
+ /// <inheritdoc />
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _userManager.OnUserUpdated += OnUserUpdated;
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _userManager.OnUserUpdated -= OnUserUpdated;
+
+ return Task.CompletedTask;
+ }
+
+ private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+ {
+ var user = e.Argument;
+ if (!user.HasPermission(PermissionKind.EnableAllDevices))
+ {
+ await UpdateDeviceAccess(user).ConfigureAwait(false);
+ }
+ }
+
+ private async Task UpdateDeviceAccess(User user)
+ {
+ var existing = (await _deviceManager.GetDevices(new DeviceQuery
+ {
+ UserId = user.Id
+ }).ConfigureAwait(false)).Items;
+
+ foreach (var device in existing)
+ {
+ if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
+ {
+ await _sessionManager.Logout(device).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index cc464a7a3d..c4a2bfdb87 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -11,6 +11,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Events.Users;
+using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -117,7 +118,7 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public User? GetUserById(Guid id)
{
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 93277a981e..d5b6e93b8e 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -6,6 +6,7 @@ using Emby.Server.Implementations.Session;
using Jellyfin.Api.WebSocketListeners;
using Jellyfin.Drawing;
using Jellyfin.Drawing.Skia;
+using Jellyfin.LiveTv;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
@@ -117,6 +118,9 @@ namespace Jellyfin.Server
// Jellyfin.Server.Implementations
yield return typeof(JellyfinDbContext).Assembly;
+
+ // Jellyfin.LiveTv
+ yield return typeof(LiveTvManager).Assembly;
}
}
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 1d4d97551e..21c6e6f01d 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -58,6 +58,7 @@
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\src\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
+ <ProjectReference Include="..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
</ItemGroup>
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 1030c6f5fc..558ad5b7bd 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -4,7 +4,11 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
+using Emby.Server.Implementations.EntryPoints;
using Jellyfin.Api.Middleware;
+using Jellyfin.LiveTv;
+using Jellyfin.LiveTv.EmbyTV;
+using Jellyfin.LiveTv.Extensions;
using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking;
using Jellyfin.Networking.HappyEyeballs;
@@ -16,6 +20,7 @@ using Jellyfin.Server.Infrastructure;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
+using MediaBrowser.XbmcMetadata;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
@@ -121,8 +126,15 @@ namespace Jellyfin.Server
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator();
+ services.AddLiveTvServices();
+ services.AddHostedService<LiveTvHost>();
services.AddHostedService<AutoDiscoveryHost>();
+ services.AddHostedService<PortForwardingHost>();
+ services.AddHostedService<NfoUserDataSaver>();
+ services.AddHostedService<LibraryChangedNotifier>();
+ services.AddHostedService<UserDataChangeNotifier>();
+ services.AddHostedService<RecordingNotifier>();
}
/// <summary>
diff --git a/Jellyfin.sln b/Jellyfin.sln
index 4385ac2417..30eab6cc21 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -87,6 +87,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Hls.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Keyframes.Tests", "tests\Jellyfin.MediaEncoding.Keyframes.Tests\Jellyfin.MediaEncoding.Keyframes.Tests.csproj", "{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv.Tests", "tests\Jellyfin.LiveTv.Tests\Jellyfin.LiveTv.Tests.csproj", "{C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj", "{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -233,6 +237,14 @@ Global
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -259,6 +271,8 @@ Global
{08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
+ {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs
deleted file mode 100644
index 0ba46ea3ba..0000000000
--- a/MediaBrowser.Common/Progress/ActionableProgress.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable CA1003
-
-using System;
-
-namespace MediaBrowser.Common.Progress
-{
- /// <summary>
- /// Class ActionableProgress.
- /// </summary>
- /// <typeparam name="T">The type for the action parameter.</typeparam>
- public class ActionableProgress<T> : IProgress<T>
- {
- /// <summary>
- /// The _actions.
- /// </summary>
- private Action<T>? _action;
-
- public event EventHandler<T>? ProgressChanged;
-
- /// <summary>
- /// Registers the action.
- /// </summary>
- /// <param name="action">The action.</param>
- public void RegisterAction(Action<T> action)
- {
- _action = action;
- }
-
- public void Report(T value)
- {
- ProgressChanged?.Invoke(this, value);
-
- _action?.Invoke(value);
- }
- }
-}
diff --git a/MediaBrowser.Common/Progress/SimpleProgress.cs b/MediaBrowser.Common/Progress/SimpleProgress.cs
deleted file mode 100644
index 7071f2bc3d..0000000000
--- a/MediaBrowser.Common/Progress/SimpleProgress.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-#pragma warning disable CA1003
-
-using System;
-
-namespace MediaBrowser.Common.Progress
-{
- public class SimpleProgress<T> : IProgress<T>
- {
- public event EventHandler<T>? ProgressChanged;
-
- public void Report(T value)
- {
- ProgressChanged?.Invoke(this, value);
- }
- }
-}
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index 94418683b2..f186523b9a 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -9,7 +9,6 @@ using System.Text.Json.Serialization;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Querying;
@@ -53,7 +52,7 @@ namespace MediaBrowser.Controller.Channels
query.ChannelIds = new Guid[] { Id };
// Don't blow up here because it could cause parent screens with other content to fail
- return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).GetAwaiter().GetResult();
+ return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
}
catch
{
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index 8eb27888ab..c8b432ecb2 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -95,12 +95,5 @@ namespace MediaBrowser.Controller.Channels
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The item media sources.</returns>
IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
-
- /// <summary>
- /// Whether the item supports media probe.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>Whether media probe should be enabled.</returns>
- bool EnableMediaProbe(BaseItem item);
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
deleted file mode 100644
index f4c3057993..0000000000
--- a/MediaBrowser.Controller/Drawing/ImageStream.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma warning disable CA1711, CS1591
-
-using System;
-using System.IO;
-using MediaBrowser.Model.Drawing;
-
-namespace MediaBrowser.Controller.Drawing
-{
- public class ImageStream : IDisposable
- {
- public ImageStream(Stream stream)
- {
- Stream = stream;
- }
-
- /// <summary>
- /// Gets the stream.
- /// </summary>
- /// <value>The stream.</value>
- public Stream Stream { get; }
-
- /// <summary>
- /// Gets or sets the format.
- /// </summary>
- /// <value>The format.</value>
- public ImageFormat Format { get; set; }
-
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- Stream?.Dispose();
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index d789033f16..b225f22df0 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -184,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
/// <exception cref="ArgumentNullException">The id is empty.</exception>
public BaseItem FindVirtualChild(Guid id)
{
- if (id.Equals(default))
+ if (id.IsEmpty())
{
throw new ArgumentNullException(nameof(id));
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 18d948a62f..11cdf84445 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
{
[JsonIgnore]
- public bool IsAccessedByName => ParentId.Equals(default);
+ public bool IsAccessedByName => ParentId.IsEmpty();
[JsonIgnore]
public override bool IsFolder => !IsAccessedByName;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index fdbceac961..ddcc994a0b 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -240,7 +240,7 @@ namespace MediaBrowser.Controller.Entities
{
get
{
- if (!ChannelId.Equals(default))
+ if (!ChannelId.IsEmpty())
{
return SourceType.Channel;
}
@@ -530,7 +530,7 @@ namespace MediaBrowser.Controller.Entities
get
{
var id = DisplayParentId;
- if (id.Equals(default))
+ if (id.IsEmpty())
{
return null;
}
@@ -746,7 +746,7 @@ namespace MediaBrowser.Controller.Entities
public virtual bool StopRefreshIfLocalMetadataFound => true;
[JsonIgnore]
- protected virtual bool SupportsOwnedItems => !ParentId.Equals(default) && IsFileProtocol;
+ protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
[JsonIgnore]
public virtual bool SupportsPeople => false;
@@ -823,7 +823,7 @@ namespace MediaBrowser.Controller.Entities
public BaseItem GetOwner()
{
var ownerId = OwnerId;
- return ownerId.Equals(default) ? null : LibraryManager.GetItemById(ownerId);
+ return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
}
public bool CanDelete(User user, List<Folder> allCollectionFolders)
@@ -968,7 +968,7 @@ namespace MediaBrowser.Controller.Entities
public BaseItem GetParent()
{
var parentId = ParentId;
- if (parentId.Equals(default))
+ if (parentId.IsEmpty())
{
return null;
}
@@ -1361,7 +1361,7 @@ namespace MediaBrowser.Controller.Entities
var tasks = extras.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
- if (!i.OwnerId.Equals(ownerId) || !i.ParentId.Equals(default))
+ if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
{
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
@@ -1673,7 +1673,7 @@ namespace MediaBrowser.Controller.Entities
// First get using the cached Id
if (info.ItemId.HasValue)
{
- if (info.ItemId.Value.Equals(default))
+ if (info.ItemId.Value.IsEmpty())
{
return null;
}
@@ -2439,7 +2439,7 @@ namespace MediaBrowser.Controller.Entities
return Task.FromResult(true);
}
- if (video.OwnerId.Equals(default))
+ if (video.OwnerId.IsEmpty())
{
video.OwnerId = this.Id;
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index e707eedbf6..1f13c833b6 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -12,7 +12,7 @@ using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Progress;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
@@ -198,7 +198,7 @@ namespace MediaBrowser.Controller.Entities
{
item.SetParent(this);
- if (item.Id.Equals(default))
+ if (item.Id.IsEmpty())
{
item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
}
@@ -428,16 +428,22 @@ namespace MediaBrowser.Controller.Entities
if (recursive)
{
- var innerProgress = new ActionableProgress<double>();
-
var folder = this;
- innerProgress.RegisterAction(innerPercent =>
+ var innerProgress = new Progress<double>(innerPercent =>
{
var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent);
progress.Report(percent);
- ProviderManager.OnRefreshProgress(folder, 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");
+ }
});
if (validChildrenNeedGeneration)
@@ -460,10 +466,8 @@ namespace MediaBrowser.Controller.Entities
var container = this as IMetadataContainer;
- var innerProgress = new ActionableProgress<double>();
-
var folder = this;
- innerProgress.RegisterAction(innerPercent =>
+ var innerProgress = new Progress<double>(innerPercent =>
{
var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent);
@@ -471,7 +475,15 @@ namespace MediaBrowser.Controller.Entities
if (recursive)
{
- ProviderManager.OnRefreshProgress(folder, 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");
+ }
}
});
@@ -571,9 +583,7 @@ namespace MediaBrowser.Controller.Entities
var actionBlock = new ActionBlock<int>(
async i =>
{
- var innerProgress = new ActionableProgress<double>();
-
- innerProgress.RegisterAction(innerPercent =>
+ var innerProgress = new Progress<double>(innerPercent =>
{
// round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
var innerPercentRounded = Math.Round(innerPercent);
@@ -697,7 +707,7 @@ namespace MediaBrowser.Controller.Entities
if (this is not UserRootFolder
&& this is not AggregateFolder
- && query.ParentId.Equals(default))
+ && query.ParentId.IsEmpty())
{
query.Parent = this;
}
@@ -840,7 +850,7 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
+ if (!query.AdjacentTo.IsNullOrEmpty())
{
Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
return true;
@@ -921,7 +931,7 @@ namespace MediaBrowser.Controller.Entities
query.ChannelIds = new[] { ChannelId };
// Don't blow up here because it could cause parent screens with other content to fail
- return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).GetAwaiter().GetResult();
+ return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
}
catch
{
@@ -987,7 +997,7 @@ namespace MediaBrowser.Controller.Entities
#pragma warning restore CA1309
// This must be the last filter
- if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
+ if (!query.AdjacentTo.IsNullOrEmpty())
{
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index bf31508c1d..37e2414142 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -74,12 +75,12 @@ namespace MediaBrowser.Controller.Entities.TV
get
{
var seriesId = SeriesId;
- if (seriesId.Equals(default))
+ if (seriesId.IsEmpty())
{
seriesId = FindSeriesId();
}
- return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series);
+ return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series);
}
}
@@ -89,12 +90,12 @@ namespace MediaBrowser.Controller.Entities.TV
get
{
var seasonId = SeasonId;
- if (seasonId.Equals(default))
+ if (seasonId.IsEmpty())
{
seasonId = FindSeasonId();
}
- return seasonId.Equals(default) ? null : (LibraryManager.GetItemById(seasonId) as Season);
+ return seasonId.IsEmpty() ? null : (LibraryManager.GetItemById(seasonId) as Season);
}
}
@@ -271,7 +272,7 @@ namespace MediaBrowser.Controller.Entities.TV
var seasonId = SeasonId;
- if (!seasonId.Equals(default) && !list.Contains(seasonId))
+ if (!seasonId.IsEmpty() && !list.Contains(seasonId))
{
list.Add(seasonId);
}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 0a040a3c28..c29cefc15e 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Querying;
@@ -48,12 +49,12 @@ namespace MediaBrowser.Controller.Entities.TV
get
{
var seriesId = SeriesId;
- if (seriesId.Equals(default))
+ if (seriesId.IsEmpty())
{
seriesId = FindSeriesId();
}
- return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series);
+ return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series);
}
}
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index eb026deb4f..c93488a85e 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -70,11 +70,11 @@ namespace MediaBrowser.Controller.Entities
/// <inheritdoc />
public override IEnumerable<Guid> GetIdsForAncestorQuery()
{
- if (!DisplayParentId.Equals(default))
+ if (!DisplayParentId.IsEmpty())
{
yield return DisplayParentId;
}
- else if (!ParentId.Equals(default))
+ else if (!ParentId.IsEmpty())
{
yield return ParentId;
}
@@ -95,11 +95,11 @@ namespace MediaBrowser.Controller.Entities
{
var parent = this as Folder;
- if (!DisplayParentId.Equals(default))
+ if (!DisplayParentId.IsEmpty())
{
parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent;
}
- else if (!ParentId.Equals(default))
+ else if (!ParentId.IsEmpty())
{
parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index a3525c8623..4af000557e 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -433,7 +433,7 @@ namespace MediaBrowser.Controller.Entities
var user = query.User;
// This must be the last filter
- if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
+ if (!query.AdjacentTo.IsNullOrEmpty())
{
items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index be2eb4d288..5adadec390 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -456,7 +456,7 @@ namespace MediaBrowser.Controller.Entities
foreach (var child in LinkedAlternateVersions)
{
// Reset the cached value
- if (child.ItemId.HasValue && child.ItemId.Value.Equals(default))
+ if (child.ItemId.IsNullOrEmpty())
{
child.ItemId = null;
}
diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs
index de74aa5a11..6d2f5b8738 100644
--- a/MediaBrowser.Controller/Library/ILibraryMonitor.cs
+++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs
@@ -1,10 +1,9 @@
-#pragma warning disable CS1591
-
-using System;
-
namespace MediaBrowser.Controller.Library
{
- public interface ILibraryMonitor : IDisposable
+ /// <summary>
+ /// Service responsible for monitoring library filesystems for changes.
+ /// </summary>
+ public interface ILibraryMonitor
{
/// <summary>
/// Starts this instance.
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index f1758a9d80..bace703ada 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -117,6 +118,14 @@ namespace MediaBrowser.Controller.Library
public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId);
/// <summary>
+ /// Gets the media sources for an active recording.
+ /// </summary>
+ /// <param name="info">The <see cref="ActiveRecordingInfo"/>.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
+ /// <returns>A task containing the <see cref="MediaSourceInfo"/>'s for the recording.</returns>
+ Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken);
+
+ /// <summary>
/// Closes the media source.
/// </summary>
/// <param name="id">The live stream identifier.</param>
diff --git a/MediaBrowser.Controller/LiveTv/IGuideManager.cs b/MediaBrowser.Controller/LiveTv/IGuideManager.cs
new file mode 100644
index 0000000000..9883b9283c
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/IGuideManager.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv;
+
+/// <summary>
+/// Service responsible for managing the Live TV guide.
+/// </summary>
+public interface IGuideManager
+{
+ /// <summary>
+ /// Gets the guide information.
+ /// </summary>
+ /// <returns>The <see cref="GuideInfo"/>.</returns>
+ GuideInfo GetGuideInfo();
+
+ /// <summary>
+ /// Refresh the guide.
+ /// </summary>
+ /// <param name="progress">The <see cref="IProgress{T}"/> to use.</param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+ /// <returns>Task representing the refresh operation.</returns>
+ Task RefreshGuide(IProgress<double> progress, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 4206159e7b..7da455b8d4 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -68,14 +68,6 @@ namespace MediaBrowser.Controller.LiveTv
Task CancelSeriesTimer(string id);
/// <summary>
- /// Adds the parts.
- /// </summary>
- /// <param name="services">The services.</param>
- /// <param name="tunerHosts">The tuner hosts.</param>
- /// <param name="listingProviders">The listing providers.</param>
- void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders);
-
- /// <summary>
/// Gets the timer.
/// </summary>
/// <param name="id">The identifier.</param>
@@ -176,12 +168,6 @@ namespace MediaBrowser.Controller.LiveTv
Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken);
/// <summary>
- /// Gets the guide information.
- /// </summary>
- /// <returns>GuideInfo.</returns>
- GuideInfo GetGuideInfo();
-
- /// <summary>
/// Gets the recommended programs.
/// </summary>
/// <param name="query">The query.</param>
@@ -254,14 +240,6 @@ namespace MediaBrowser.Controller.LiveTv
Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
/// <summary>
- /// Saves the tuner host.
- /// </summary>
- /// <param name="info">Turner host to save.</param>
- /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
- /// <returns>Tuner host information wrapped in a task.</returns>
- Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
-
- /// <summary>
/// Saves the listing provider.
/// </summary>
/// <param name="info">The information.</param>
@@ -298,10 +276,6 @@ namespace MediaBrowser.Controller.LiveTv
Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
- List<NameIdPair> GetTunerHostTypes();
-
- Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
-
string GetEmbyTvActiveRecordingPath(string id);
ActiveRecordingInfo GetActiveRecordingInfo(string path);
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
index ce34954e3f..52fb156481 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
@@ -141,14 +141,6 @@ namespace MediaBrowser.Controller.LiveTv
Task CloseLiveStream(string id, CancellationToken cancellationToken);
/// <summary>
- /// Records the live stream.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task RecordLiveStream(string id, CancellationToken cancellationToken);
-
- /// <summary>
/// Resets the tuner.
/// </summary>
/// <param name="id">The identifier.</param>
@@ -180,9 +172,4 @@ namespace MediaBrowser.Controller.LiveTv
{
Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
}
-
- public interface ISupportsUpdatingDefaults
- {
- Task UpdateTimerDefaults(SeriesTimerInfo info, CancellationToken cancellationToken);
- }
}
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
index b983091588..3689a2adf6 100644
--- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -36,13 +36,6 @@ namespace MediaBrowser.Controller.LiveTv
Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken);
/// <summary>
- /// Gets the tuner infos.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task&lt;List&lt;LiveTvTunerInfo&gt;&gt;.</returns>
- Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken);
-
- /// <summary>
/// Gets the channel stream.
/// </summary>
/// <param name="channelId">The channel identifier.</param>
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs b/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs
new file mode 100644
index 0000000000..3df6066f66
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv;
+
+/// <summary>
+/// Service responsible for managing the <see cref="ITunerHost"/>s.
+/// </summary>
+public interface ITunerHostManager
+{
+ /// <summary>
+ /// Gets the available <see cref="ITunerHost"/>s.
+ /// </summary>
+ IReadOnlyList<ITunerHost> TunerHosts { get; }
+
+ /// <summary>
+ /// Gets the <see cref="NameIdPair"/>s for the available <see cref="ITunerHost"/>s.
+ /// </summary>
+ /// <returns>The <see cref="NameIdPair"/>s.</returns>
+ IEnumerable<NameIdPair> GetTunerHostTypes();
+
+ /// <summary>
+ /// Saves the tuner host.
+ /// </summary>
+ /// <param name="info">Turner host to save.</param>
+ /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
+ /// <returns>Tuner host information wrapped in a task.</returns>
+ Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
+
+ /// <summary>
+ /// Discovers the available tuners.
+ /// </summary>
+ /// <param name="newDevicesOnly">A value indicating whether to only return new devices.</param>
+ /// <returns>The <see cref="TunerHostInfo"/>s.</returns>
+ IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly);
+
+ /// <summary>
+ /// Scans for tuner devices that have changed URLs.
+ /// </summary>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+ /// <returns>A task that represents the scanning operation.</returns>
+ Task ScanForTunerDeviceChanges(CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
deleted file mode 100644
index eb3babc180..0000000000
--- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
- public class LiveTvServiceStatusInfo
- {
- public LiveTvServiceStatusInfo()
- {
- Tuners = new List<LiveTvTunerInfo>();
- IsVisible = true;
- }
-
- /// <summary>
- /// Gets or sets the status.
- /// </summary>
- /// <value>The status.</value>
- public LiveTvServiceStatus Status { get; set; }
-
- /// <summary>
- /// Gets or sets the status message.
- /// </summary>
- /// <value>The status message.</value>
- public string StatusMessage { get; set; }
-
- /// <summary>
- /// Gets or sets the version.
- /// </summary>
- /// <value>The version.</value>
- public string Version { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance has update available.
- /// </summary>
- /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
- public bool HasUpdateAvailable { get; set; }
-
- /// <summary>
- /// Gets or sets the tuners.
- /// </summary>
- /// <value>The tuners.</value>
- public List<LiveTvTunerInfo> Tuners { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is visible.
- /// </summary>
- /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value>
- public bool IsVisible { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
deleted file mode 100644
index aa5eb59d16..0000000000
--- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
- public class LiveTvTunerInfo
- {
- public LiveTvTunerInfo()
- {
- Clients = new List<string>();
- }
-
- /// <summary>
- /// Gets or sets the type of the source.
- /// </summary>
- /// <value>The type of the source.</value>
- public string SourceType { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- public string Url { get; set; }
-
- /// <summary>
- /// Gets or sets the status.
- /// </summary>
- /// <value>The status.</value>
- public LiveTvTunerStatus Status { get; set; }
-
- /// <summary>
- /// Gets or sets the channel identifier.
- /// </summary>
- /// <value>The channel identifier.</value>
- public string ChannelId { get; set; }
-
- /// <summary>
- /// Gets or sets the recording identifier.
- /// </summary>
- /// <value>The recording identifier.</value>
- public string RecordingId { get; set; }
-
- /// <summary>
- /// Gets or sets the name of the program.
- /// </summary>
- /// <value>The name of the program.</value>
- public string ProgramName { get; set; }
-
- /// <summary>
- /// Gets or sets the clients.
- /// </summary>
- /// <value>The clients.</value>
- public List<string> Clients { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance can reset.
- /// </summary>
- /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
- public bool CanReset { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
deleted file mode 100644
index 1dcf7a58fe..0000000000
--- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
+++ /dev/null
@@ -1,210 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
- public class RecordingInfo
- {
- public RecordingInfo()
- {
- Genres = new List<string>();
- }
-
- /// <summary>
- /// Gets or sets the id of the recording.
- /// </summary>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the series timer identifier.
- /// </summary>
- /// <value>The series timer identifier.</value>
- public string SeriesTimerId { get; set; }
-
- /// <summary>
- /// Gets or sets the timer identifier.
- /// </summary>
- /// <value>The timer identifier.</value>
- public string TimerId { get; set; }
-
- /// <summary>
- /// Gets or sets the channelId of the recording.
- /// </summary>
- public string ChannelId { get; set; }
-
- /// <summary>
- /// Gets or sets the type of the channel.
- /// </summary>
- /// <value>The type of the channel.</value>
- public ChannelType ChannelType { get; set; }
-
- /// <summary>
- /// Gets or sets the name of the recording.
- /// </summary>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the path.
- /// </summary>
- /// <value>The path.</value>
- public string Path { get; set; }
-
- /// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- public string Url { get; set; }
-
- /// <summary>
- /// Gets or sets the overview.
- /// </summary>
- /// <value>The overview.</value>
- public string Overview { get; set; }
-
- /// <summary>
- /// Gets or sets the start date of the recording, in UTC.
- /// </summary>
- public DateTime StartDate { get; set; }
-
- /// <summary>
- /// Gets or sets the end date of the recording, in UTC.
- /// </summary>
- public DateTime EndDate { get; set; }
-
- /// <summary>
- /// Gets or sets the program identifier.
- /// </summary>
- /// <value>The program identifier.</value>
- public string ProgramId { get; set; }
-
- /// <summary>
- /// Gets or sets the status.
- /// </summary>
- /// <value>The status.</value>
- public RecordingStatus Status { get; set; }
-
- /// <summary>
- /// Gets or sets the genre of the program.
- /// </summary>
- public List<string> Genres { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is repeat.
- /// </summary>
- /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
- public bool IsRepeat { get; set; }
-
- /// <summary>
- /// Gets or sets the episode title.
- /// </summary>
- /// <value>The episode title.</value>
- public string EpisodeTitle { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is hd.
- /// </summary>
- /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
- public bool? IsHD { get; set; }
-
- /// <summary>
- /// Gets or sets the audio.
- /// </summary>
- /// <value>The audio.</value>
- public ProgramAudio? Audio { get; set; }
-
- /// <summary>
- /// Gets or sets the original air date.
- /// </summary>
- /// <value>The original air date.</value>
- public DateTime? OriginalAirDate { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is movie.
- /// </summary>
- /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value>
- public bool IsMovie { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is sports.
- /// </summary>
- /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
- public bool IsSports { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is series.
- /// </summary>
- /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
- public bool IsSeries { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is live.
- /// </summary>
- /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
- public bool IsLive { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is news.
- /// </summary>
- /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
- public bool IsNews { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is kids.
- /// </summary>
- /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
- public bool IsKids { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is premiere.
- /// </summary>
- /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
- public bool IsPremiere { get; set; }
-
- /// <summary>
- /// Gets or sets the official rating.
- /// </summary>
- /// <value>The official rating.</value>
- public string OfficialRating { get; set; }
-
- /// <summary>
- /// Gets or sets the community rating.
- /// </summary>
- /// <value>The community rating.</value>
- public float? CommunityRating { get; set; }
-
- /// <summary>
- /// 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; }
-
- /// <summary>
- /// Gets or sets the image url if it can be downloaded.
- /// </summary>
- /// <value>The image URL.</value>
- public string ImageUrl { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance has image.
- /// </summary>
- /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
- public bool? HasImage { get; set; }
-
- /// <summary>
- /// Gets or sets the show identifier.
- /// </summary>
- /// <value>The show identifier.</value>
- public string ShowId { get; set; }
-
- /// <summary>
- /// Gets or sets the date last updated.
- /// </summary>
- /// <value>The date last updated.</value>
- public DateTime DateLastUpdated { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
deleted file mode 100644
index 0b943c9396..0000000000
--- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
- public class RecordingStatusChangedEventArgs : EventArgs
- {
- public string RecordingId { get; set; }
-
- public RecordingStatus NewStatus { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index fb4e7bd1f5..29dd190ab7 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -88,6 +88,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string Level { get; set; }
/// <summary>
+ /// Gets or sets the codec tag.
+ /// </summary>
+ /// <value>The codec tag.</value>
+ public string CodecTag { get; set; }
+
+ /// <summary>
/// Gets or sets the framerate.
/// </summary>
/// <value>The framerate.</value>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 400e7f40fb..1c95192f18 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -30,6 +30,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private const string VaapiAlias = "va";
private const string D3d11vaAlias = "dx11";
private const string VideotoolboxAlias = "vt";
+ private const string RkmppAlias = "rk";
private const string OpenclAlias = "ocl";
private const string CudaAlias = "cu";
private const string DrmAlias = "dr";
@@ -161,6 +162,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "vaapi", hwEncoder + "_vaapi" },
{ "videotoolbox", hwEncoder + "_videotoolbox" },
{ "v4l2m2m", hwEncoder + "_v4l2m2m" },
+ { "rkmpp", hwEncoder + "_rkmpp" },
};
if (!string.IsNullOrEmpty(hwType)
@@ -217,6 +219,14 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.SupportsFilter("hwupload_vaapi");
}
+ private bool IsRkmppFullSupported()
+ {
+ return _mediaEncoder.SupportsHwaccel("rkmpp")
+ && _mediaEncoder.SupportsFilter("scale_rkrga")
+ && _mediaEncoder.SupportsFilter("vpp_rkrga")
+ && _mediaEncoder.SupportsFilter("overlay_rkrga");
+ }
+
private bool IsOpenclFullSupported()
{
return _mediaEncoder.SupportsHwaccel("opencl")
@@ -696,6 +706,14 @@ namespace MediaBrowser.Controller.MediaEncoding
return codec.ToLowerInvariant();
}
+ private string GetRkmppDeviceArgs(string alias)
+ {
+ alias ??= RkmppAlias;
+
+ // device selection in rk is not supported.
+ return " -init_hw_device rkmpp=" + alias;
+ }
+
private string GetVideoToolboxDeviceArgs(string alias)
{
alias ??= VideotoolboxAlias;
@@ -835,30 +853,25 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
{
- // DVBSUB and DVDSUB use the fixed canvas size 720x576
+ // DVBSUB uses the fixed canvas size 720x576
if (state.SubtitleStream is not null
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
&& !state.SubtitleStream.IsTextSubtitleStream
- && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase))
+ && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase))
{
- var inW = state.VideoStream?.Width;
- var inH = state.VideoStream?.Height;
- var reqW = state.BaseRequest.Width;
- var reqH = state.BaseRequest.Height;
- var reqMaxW = state.BaseRequest.MaxWidth;
- var reqMaxH = state.BaseRequest.MaxHeight;
-
- // setup a relative small canvas_size for overlay_qsv/vaapi to reduce transfer overhead
- var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, 1080);
+ var subtitleWidth = state.SubtitleStream?.Width;
+ var subtitleHeight = state.SubtitleStream?.Height;
- if (overlayW.HasValue && overlayH.HasValue)
+ if (subtitleWidth.HasValue
+ && subtitleHeight.HasValue
+ && subtitleWidth.Value > 0
+ && subtitleHeight.Value > 0)
{
return string.Format(
CultureInfo.InvariantCulture,
" -canvas_size {0}x{1}",
- overlayW.Value,
- overlayH.Value);
+ subtitleWidth.Value,
+ subtitleHeight.Value);
}
}
@@ -1061,6 +1074,33 @@ namespace MediaBrowser.Controller.MediaEncoding
// no videotoolbox hw filter.
args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias));
}
+ else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!isLinux || !_mediaEncoder.SupportsHwaccel("rkmpp"))
+ {
+ return string.Empty;
+ }
+
+ var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
+ var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
+ if (!isRkmppDecoder && !isRkmppEncoder)
+ {
+ return string.Empty;
+ }
+
+ args.Append(GetRkmppDeviceArgs(RkmppAlias));
+
+ var filterDevArgs = string.Empty;
+ var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
+
+ if (doOclTonemap && !isRkmppDecoder)
+ {
+ args.Append(GetOpenclDeviceArgs(0, null, RkmppAlias, OpenclAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ }
+
+ args.Append(filterDevArgs);
+ }
if (!string.IsNullOrEmpty(vidDecoder))
{
@@ -1477,8 +1517,10 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h264_rkmpp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc_rkmpp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase)
@@ -1918,20 +1960,22 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_baseline";
}
- // libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case.
+ // libx264, h264_{qsv,nvenc,rkmpp} does not support Constrained Baseline profile, force Baseline in this case.
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_rkmpp", StringComparison.OrdinalIgnoreCase))
&& profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
{
profile = "baseline";
}
- // libx264, h264_qsv, h264_nvenc and h264_vaapi does not support Constrained High profile, force High in this case.
+ // libx264, h264_{qsv,nvenc,vaapi,rkmpp} does not support Constrained High profile, force High in this case.
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_rkmpp", StringComparison.OrdinalIgnoreCase))
&& profile.Contains("high", StringComparison.OrdinalIgnoreCase))
{
profile = "high";
@@ -2015,6 +2059,11 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -level " + level;
}
}
+ else if (string.Equals(videoEncoder, "h264_rkmpp", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -level " + level;
+ }
else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
@@ -2833,6 +2882,48 @@ namespace MediaBrowser.Controller.MediaEncoding
return (outputWidth, outputHeight);
}
+ public static bool IsScaleRatioSupported(
+ int? videoWidth,
+ int? videoHeight,
+ int? requestedWidth,
+ int? requestedHeight,
+ int? requestedMaxWidth,
+ int? requestedMaxHeight,
+ double? maxScaleRatio)
+ {
+ var (outWidth, outHeight) = GetFixedOutputSize(
+ videoWidth,
+ videoHeight,
+ requestedWidth,
+ requestedHeight,
+ requestedMaxWidth,
+ requestedMaxHeight);
+
+ if (!videoWidth.HasValue
+ || !videoHeight.HasValue
+ || !outWidth.HasValue
+ || !outHeight.HasValue
+ || !maxScaleRatio.HasValue
+ || (maxScaleRatio.Value < 1.0f))
+ {
+ return false;
+ }
+
+ var minScaleRatio = 1.0f / maxScaleRatio;
+ var scaleRatioW = (double)outWidth / (double)videoWidth;
+ var scaleRatioH = (double)outHeight / (double)videoHeight;
+
+ if (scaleRatioW < minScaleRatio
+ || scaleRatioW > maxScaleRatio
+ || scaleRatioH < minScaleRatio
+ || scaleRatioH > maxScaleRatio)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
public static string GetHwScaleFilter(
string hwScaleSuffix,
string videoFormat,
@@ -2877,7 +2968,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- public static string GetCustomSwScaleFilter(
+ public static string GetGraphicalSubPreProcessFilters(
int? videoWidth,
int? videoHeight,
int? requestedWidth,
@@ -2897,7 +2988,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return string.Format(
CultureInfo.InvariantCulture,
- "scale=s={0}x{1}:flags=fast_bilinear",
+ @"scale=-1:{1}:fast_bilinear,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
outWidth.Value,
outHeight.Value);
}
@@ -2913,7 +3004,7 @@ namespace MediaBrowser.Controller.MediaEncoding
int? requestedHeight,
int? requestedMaxWidth,
int? requestedMaxHeight,
- int? framerate)
+ float? framerate)
{
var reqTicks = state.BaseRequest.StartTimeTicks ?? 0;
var startTime = TimeSpan.FromTicks(reqTicks).ToString(@"hh\\\:mm\\\:ss\\\.fff", CultureInfo.InvariantCulture);
@@ -2932,7 +3023,7 @@ namespace MediaBrowser.Controller.MediaEncoding
"alphasrc=s={0}x{1}:r={2}:start='{3}'",
outWidth.Value,
outHeight.Value,
- framerate ?? 10,
+ framerate ?? 25,
reqTicks > 0 ? startTime : 0);
}
@@ -3340,9 +3431,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (hasGraphicalSubs)
{
- // [0:s]scale=s=1280x720
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
@@ -3504,15 +3594,17 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- // scale=s=1280x720,format=yuva420p,hwupload
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
subFilters.Add("format=yuva420p");
}
else if (hasTextSubs)
{
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
// alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=yuva420p");
@@ -3527,8 +3619,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -3702,15 +3794,17 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- // scale=s=1280x720,format=yuva420p,hwupload
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
subFilters.Add("format=yuva420p");
}
else if (hasTextSubs)
{
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
// alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=yuva420p");
@@ -3727,8 +3821,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -3938,16 +4032,18 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- // scale,format=bgra,hwupload
- // overlay_qsv can handle overlay scaling,
- // add a dummy scale filter to pair with -canvas_size.
- subFilters.Add("scale=flags=fast_bilinear");
+ // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
+ subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
else if (hasTextSubs)
{
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
// alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, hasAssSubs ? 10 : 5);
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -3973,8 +4069,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -4158,12 +4254,17 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- subFilters.Add("scale=flags=fast_bilinear");
+ // overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
+ subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
else if (hasTextSubs)
{
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, hasAssSubs ? 10 : 5);
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -4189,8 +4290,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
}
@@ -4425,12 +4526,17 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- subFilters.Add("scale=flags=fast_bilinear");
+ // overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
+ subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
else if (hasTextSubs)
{
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, hasAssSubs ? 10 : 5);
+ var framerate = state.VideoStream?.RealFrameRate;
+ var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
+
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
subFilters.Add(alphaSrcFilter);
subFilters.Add("format=bgra");
@@ -4454,8 +4560,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder)
@@ -4599,14 +4705,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- // scale=s=1280x720,format=bgra,hwupload
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
subFilters.Add("format=bgra");
}
else if (hasTextSubs)
{
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
+ 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");
@@ -4815,8 +4923,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (hasGraphicalSubs)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder)
@@ -4899,6 +5007,237 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the parameter of Rockchip RKMPP/RKRGA filter chain.
+ /// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="vidEncoder">Video encoder to use.</param>
+ /// <returns>The tuple contains three lists: main, sub and overlay filters.</returns>
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetRkmppVidFilterChain(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidEncoder)
+ {
+ if (!string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ return (null, null, null);
+ }
+
+ var isLinux = OperatingSystem.IsLinux();
+ var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
+ var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
+ var isSwEncoder = !vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
+ var isRkmppOclSupported = isLinux && IsRkmppFullSupported() && IsOpenclFullSupported();
+
+ if ((isSwDecoder && isSwEncoder)
+ || !isRkmppOclSupported
+ || !_mediaEncoder.SupportsFilter("alphasrc"))
+ {
+ return GetSwVidFilterChain(state, options, vidEncoder);
+ }
+
+ // prefered rkmpp + rkrga + opencl filters pipeline
+ if (isRkmppOclSupported)
+ {
+ return GetRkmppVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
+ }
+
+ return (null, null, null);
+ }
+
+ public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetRkmppVidFiltersPrefered(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string vidDecoder,
+ string vidEncoder)
+ {
+ var inW = state.VideoStream?.Width;
+ var inH = state.VideoStream?.Height;
+ var reqW = state.BaseRequest.Width;
+ var reqH = state.BaseRequest.Height;
+ var reqMaxW = state.BaseRequest.MaxWidth;
+ var reqMaxH = state.BaseRequest.MaxHeight;
+ var threeDFormat = state.MediaSource.Video3DFormat;
+
+ var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
+ var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
+ var isSwDecoder = !isRkmppDecoder;
+ var isSwEncoder = !isRkmppEncoder;
+ var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder;
+
+ 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 doOclTonemap = IsHwTonemapAvailable(state, options);
+
+ var hasSubs = state.SubtitleStream != 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));
+
+ /* Make main filters for video stream */
+ var mainFilters = new List<string>();
+
+ mainFilters.Add(GetOverwriteColorPropertiesParam(state, doOclTonemap));
+
+ if (isSwDecoder)
+ {
+ // INPUT sw surface(memory)
+ // sw deint
+ if (doDeintH2645)
+ {
+ var swDeintFilter = GetSwDeinterlaceFilter(state, options);
+ mainFilters.Add(swDeintFilter);
+ }
+
+ var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ if (!string.IsNullOrEmpty(swScaleFilter))
+ {
+ swScaleFilter += ":flags=fast_bilinear";
+ }
+
+ // sw scale
+ mainFilters.Add(swScaleFilter);
+ mainFilters.Add("format=" + outFormat);
+
+ // keep video at memory except ocl tonemap,
+ // since the overhead caused by hwupload >>> using sw filter.
+ // sw => hw
+ if (doOclTonemap)
+ {
+ mainFilters.Add("hwupload=derive_device=opencl");
+ }
+ }
+ else if (isRkmppDecoder)
+ {
+ // INPUT rkmpp/drm surface(gem/dma-heap)
+
+ var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap;
+ var outFormat = doOclTonemap ? "p010" : "nv12";
+ var hwScaleFilter = GetHwScaleFilter("rkrga", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ var hwScaleFilter2 = GetHwScaleFilter("rkrga", string.Empty, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ if (!hasSubs
+ || !isFullAfbcPipeline
+ || !string.IsNullOrEmpty(hwScaleFilter2))
+ {
+ // try enabling AFBC to save DDR bandwidth
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isFullAfbcPipeline)
+ {
+ hwScaleFilter += ":afbc=1";
+ }
+
+ // hw scale
+ mainFilters.Add(hwScaleFilter);
+ }
+ }
+
+ if (doOclTonemap && isRkmppDecoder)
+ {
+ // map from rkmpp/drm to opencl via drm-opencl interop.
+ mainFilters.Add("hwmap=derive_device=opencl:mode=read");
+ }
+
+ // ocl tonemap
+ if (doOclTonemap)
+ {
+ var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
+ // enable tradeoffs for performance
+ if (!string.IsNullOrEmpty(tonemapFilter))
+ {
+ tonemapFilter += ":tradeoff=1";
+ }
+
+ mainFilters.Add(tonemapFilter);
+ }
+
+ var memoryOutput = false;
+ var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
+ if ((isRkmppDecoder && isSwEncoder) || isUploadForOclTonemap)
+ {
+ memoryOutput = true;
+
+ // OUTPUT nv12 surface(memory)
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
+ }
+
+ // OUTPUT nv12 surface(memory)
+ if (isSwDecoder && isRkmppEncoder)
+ {
+ memoryOutput = true;
+ }
+
+ if (memoryOutput)
+ {
+ // text subtitles
+ if (hasTextSubs)
+ {
+ var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
+ mainFilters.Add(textSubtitlesFilter);
+ }
+ }
+
+ if (isDrmInDrmOut)
+ {
+ if (doOclTonemap)
+ {
+ // OUTPUT drm(nv12) surface(gem/dma-heap)
+ // reverse-mapping via drm-opencl interop.
+ mainFilters.Add("hwmap=derive_device=rkmpp:mode=write:reverse=1");
+ mainFilters.Add("format=drm_prime");
+ }
+ }
+
+ /* Make sub and overlay filters for subtitle stream */
+ var subFilters = new List<string>();
+ var overlayFilters = new List<string>();
+ if (isDrmInDrmOut)
+ {
+ 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;
+
+ // alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
+ 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=rkmpp");
+
+ // try enabling AFBC to save DDR bandwidth
+ overlayFilters.Add("overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12:afbc=1");
+ }
+ }
+ else if (memoryOutput)
+ {
+ if (hasGraphicalSubs)
+ {
+ var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subPreProcFilters);
+ overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
+ }
+ }
+
+ return (mainFilters, subFilters, overlayFilters);
+ }
+
+ /// <summary>
/// Gets the parameter of video processing filters.
/// </summary>
/// <param name="state">Encoding state.</param>
@@ -4944,6 +5283,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
(mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec);
}
+ else if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ (mainFilters, subFilters, overlayFilters) = GetRkmppVidFilterChain(state, options, outputVideoCodec);
+ }
else
{
(mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec);
@@ -5075,18 +5418,21 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
{
return 8;
}
if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
{
return 10;
}
if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
{
return 12;
@@ -5139,7 +5485,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)))
{
- return null;
+ // One exception is that RKMPP decoder can handle H.264 High 10.
+ if (!(string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)))
+ {
+ return null;
+ }
}
if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
@@ -5166,6 +5517,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth);
}
+
+ if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetRkmppVidDecoder(state, options, videoStream, bitDepth);
+ }
}
var whichCodec = videoStream.Codec;
@@ -5231,6 +5587,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ if (string.Equals(decoderSuffix, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
return isCodecAvailable ? (" -c:v " + decoderName) : null;
}
@@ -5253,6 +5614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isCudaSupported = (isLinux || isWindows) && IsCudaFullSupported();
var isQsvSupported = (isLinux || isWindows) && _mediaEncoder.SupportsHwaccel("qsv");
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
+ var isRkmppSupported = isLinux && IsRkmppFullSupported();
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
var ffmpegVersion = _mediaEncoder.EncoderVersion;
@@ -5355,6 +5717,14 @@ namespace MediaBrowser.Controller.MediaEncoding
return " -hwaccel videotoolbox" + (outputHwSurface ? " -hwaccel_output_format videotoolbox_vld" : string.Empty);
}
+ // Rockchip rkmpp
+ if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
+ && isRkmppSupported
+ && isCodecAvailable)
+ {
+ return " -hwaccel rkmpp" + (outputHwSurface ? " -hwaccel_output_format drm_prime" : string.Empty);
+ }
+
return null;
}
@@ -5661,6 +6031,102 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
+ public string GetRkmppVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
+ {
+ var isLinux = OperatingSystem.IsLinux();
+
+ if (!isLinux
+ || !string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var inW = state.VideoStream?.Width;
+ var inH = state.VideoStream?.Height;
+ var reqW = state.BaseRequest.Width;
+ var reqH = state.BaseRequest.Height;
+ var reqMaxW = state.BaseRequest.MaxWidth;
+ var reqMaxH = state.BaseRequest.MaxHeight;
+
+ // rkrga RGA2e supports range from 1/16 to 16
+ if (!IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 16.0f))
+ {
+ return null;
+ }
+
+ var isRkmppOclSupported = IsRkmppFullSupported() && IsOpenclFullSupported();
+ var hwSurface = isRkmppOclSupported
+ && _mediaEncoder.SupportsFilter("alphasrc");
+
+ // rkrga RGA3 supports range from 1/8 to 8
+ var isAfbcSupported = hwSurface && IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
+
+ // TODO: add more 8/10bit and 4:2:2 formats for Rkmpp after finishing the ffcheck tool
+ var is8bitSwFormatsRkmpp = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
+ || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var is10bitSwFormatsRkmpp = string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var is8_10bitSwFormatsRkmpp = is8bitSwFormatsRkmpp || is10bitSwFormatsRkmpp;
+
+ // nv15 and nv20 are bit-stream only formats
+ if (is10bitSwFormatsRkmpp && !hwSurface)
+ {
+ return null;
+ }
+
+ if (is8bitSwFormatsRkmpp)
+ {
+ if (string.Equals(videoStream.Codec, "mpeg1video", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "mpeg1video", bitDepth, hwSurface);
+ }
+
+ if (string.Equals(videoStream.Codec, "mpeg2video", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "mpeg2video", bitDepth, hwSurface);
+ }
+
+ if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "mpeg4", bitDepth, hwSurface);
+ }
+
+ if (string.Equals(videoStream.Codec, "vp8", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "vp8", bitDepth, hwSurface);
+ }
+ }
+
+ if (is8_10bitSwFormatsRkmpp)
+ {
+ if (string.Equals(videoStream.Codec, "avc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ var accelType = GetHwaccelType(state, options, "h264", bitDepth, hwSurface);
+ return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
+ }
+
+ if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase))
+ {
+ var accelType = GetHwaccelType(state, options, "hevc", bitDepth, hwSurface);
+ return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
+ }
+
+ if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase))
+ {
+ var accelType = GetHwaccelType(state, options, "vp9", bitDepth, hwSurface);
+ return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
+ }
+
+ if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
+ }
+ }
+
+ return null;
+ }
+
/// <summary>
/// Gets the number of threads.
/// </summary>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 17813559a8..f2a0b906dc 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -619,6 +619,26 @@ namespace MediaBrowser.Controller.MediaEncoding
return Array.Empty<string>();
}
+ public string[] GetRequestedCodecTags(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
+ {
+ return BaseRequest.CodecTag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var codectag = BaseRequest.GetOption(codec, "codectag");
+
+ if (!string.IsNullOrEmpty(codectag))
+ {
+ return codectag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+
+ return Array.Empty<string>();
+ }
+
public string GetRequestedLevel(string codec)
{
if (!string.IsNullOrEmpty(BaseRequest.Level))
diff --git a/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
index c19a12ae7a..09bc01f748 100644
--- a/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
@@ -96,9 +96,10 @@ public interface ITranscodeManager
public void OnTranscodeEndRequest(TranscodingJob job);
/// <summary>
- /// Gets the transcoding lock.
+ /// Transcoding lock.
/// </summary>
/// <param name="outputPath">The output path of the transcoded file.</param>
- /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
- public SemaphoreSlim GetTranscodingLock(string outputPath);
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>An <see cref="IDisposable"/>.</returns>
+ ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
deleted file mode 100644
index 044ba6d331..0000000000
--- a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.MediaEncoding
-{
- public class ImageEncodingOptions
- {
- public string InputPath { get; set; }
-
- public int? Width { get; set; }
-
- public int? Height { get; set; }
-
- public int? MaxWidth { get; set; }
-
- public int? MaxHeight { get; set; }
-
- public int? Quality { get; set; }
-
- public string Format { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
deleted file mode 100644
index 841e7b2872..0000000000
--- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.MediaEncoding
-{
- /// <summary>
- /// Class MediaEncoderHelpers.
- /// </summary>
- public static class MediaEncoderHelpers
- {
- }
-}
diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
deleted file mode 100644
index 2b831103a5..0000000000
--- a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace MediaBrowser.Controller.Plugins
-{
- /// <summary>
- /// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
- /// </summary>
- public interface IRunBeforeStartup
- {
- }
-}
diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
deleted file mode 100644
index 6024661e15..0000000000
--- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Plugins
-{
- /// <summary>
- /// Represents an entry point for a module in the application. This interface is scanned for automatically and
- /// provides a hook to initialize the module at application start.
- /// The entry point can additionally be flagged as a pre-startup task by implementing the
- /// <see cref="IRunBeforeStartup"/> interface.
- /// </summary>
- public interface IServerEntryPoint : IDisposable
- {
- /// <summary>
- /// Run the initialization for this module. This method is invoked at application start.
- /// </summary>
- /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
- Task RunAsync();
- }
-}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 53df7133b5..5a47236f92 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -111,7 +111,8 @@ namespace MediaBrowser.Controller.Session
/// Reports the session ended.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
- void ReportSessionEnded(string sessionId);
+ /// <returns>Task.</returns>
+ ValueTask ReportSessionEnded(string sessionId);
/// <summary>
/// Sends the general command.
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 3e30c8dc45..3a12a56f1e 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Class SessionInfo.
/// </summary>
- public sealed class SessionInfo : IAsyncDisposable, IDisposable
+ public sealed class SessionInfo : IAsyncDisposable
{
// 1 second
private const long ProgressIncrement = 10000000;
@@ -374,26 +374,6 @@ namespace MediaBrowser.Controller.Session
}
}
- /// <inheritdoc />
- public void Dispose()
- {
- _disposed = true;
-
- StopAutomaticProgress();
-
- var controllers = SessionControllers.ToList();
- SessionControllers = Array.Empty<ISessionController>();
-
- foreach (var controller in controllers)
- {
- if (controller is IDisposable disposable)
- {
- _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
- disposable.Dispose();
- }
- }
- }
-
public async ValueTask DisposeAsync()
{
_disposed = true;
@@ -401,6 +381,7 @@ namespace MediaBrowser.Controller.Session
StopAutomaticProgress();
var controllers = SessionControllers.ToList();
+ SessionControllers = Array.Empty<ISessionController>();
foreach (var controller in controllers)
{
@@ -409,6 +390,11 @@ namespace MediaBrowser.Controller.Session
_logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name);
await disposableAsync.DisposeAsync().ConfigureAwait(false);
}
+ else if (controller is IDisposable disposable)
+ {
+ _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
+ disposable.Dispose();
+ }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 299f294b29..ff91a60a79 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -22,7 +23,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments
{
- public sealed class AttachmentExtractor : IAttachmentExtractor
+ public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
{
private readonly ILogger<AttachmentExtractor> _logger;
private readonly IApplicationPaths _appPaths;
@@ -30,8 +31,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
- private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
- new ConcurrentDictionary<string, SemaphoreSlim>();
+ private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
public AttachmentExtractor(
ILogger<AttachmentExtractor> logger,
@@ -84,11 +88,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!Directory.Exists(outputPath))
{
@@ -99,10 +99,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
public async Task ExtractAllAttachmentsExternal(
@@ -111,11 +107,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(Path.Join(outputPath, id)))
{
@@ -131,10 +123,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
}
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractAllAttachmentsInternal(
@@ -256,11 +244,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(outputPath))
{
@@ -271,10 +255,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractAttachmentInternal(
@@ -379,5 +359,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
var prefix = filename.AsSpan(0, 1);
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
}
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _semaphoreLocks.Dispose();
+ }
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 0d1d27ae8b..fdca283908 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -45,7 +45,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg4_cuvid",
"vp8_cuvid",
"vp9_cuvid",
- "av1_cuvid"
+ "av1_cuvid",
+ "h264_rkmpp",
+ "hevc_rkmpp",
+ "mpeg1_rkmpp",
+ "mpeg2_rkmpp",
+ "mpeg4_rkmpp",
+ "vp8_rkmpp",
+ "vp9_rkmpp",
+ "av1_rkmpp"
};
private static readonly string[] _requiredEncoders = new[]
@@ -82,7 +90,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
"av1_vaapi",
"h264_v4l2m2m",
"h264_videotoolbox",
- "hevc_videotoolbox"
+ "hevc_videotoolbox",
+ "h264_rkmpp",
+ "hevc_rkmpp"
};
private static readonly string[] _requiredFilters = new[]
@@ -116,9 +126,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"libplacebo",
"scale_vulkan",
"overlay_vulkan",
- "hwupload_vaapi",
// videotoolbox
- "yadif_videotoolbox"
+ "yadif_videotoolbox",
+ // rkrga
+ "scale_rkrga",
+ "vpp_rkrga",
+ "overlay_rkrga"
};
private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 4dbefca4bb..f86d14fc86 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
@@ -60,7 +61,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly IServerConfigurationManager _serverConfig;
private readonly string _startupOptionFFmpegPath;
- private readonly SemaphoreSlim _thumbnailResourcePool;
+ private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
private readonly object _runningProcessesLock = new object();
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
@@ -116,7 +117,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
var semaphoreCount = 2 * Environment.ProcessorCount;
- _thumbnailResourcePool = new SemaphoreSlim(semaphoreCount, semaphoreCount);
+ _thumbnailResourcePool = new(semaphoreCount);
}
/// <inheritdoc />
@@ -754,8 +755,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
bool ranToCompletion;
- await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -776,10 +776,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
ranToCompletion = false;
}
}
- finally
- {
- _thumbnailResourcePool.Release();
- }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
var file = _fileSystem.GetFileInfo(tempExtractPath);
@@ -908,8 +904,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
bool ranToCompletion = false;
- await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
StartProcess(processWrapper);
@@ -963,10 +958,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
StopProcess(processWrapper, 1000);
}
}
- finally
- {
- _thumbnailResourcePool.Release();
- }
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index a4e8194c15..be63513a72 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="BDInfo" />
<PackageReference Include="libse" />
<PackageReference Include="Microsoft.Extensions.Http" />
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index dba7aea6fd..317aba4186 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -755,6 +755,10 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+ // Graphical subtitle may have width and height info
+ stream.Width = streamInfo.Width;
+ stream.Height = streamInfo.Height;
+
if (string.IsNullOrEmpty(stream.Title))
{
// mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 459d854bf1..8f1cc3f645 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -27,7 +28,7 @@ using UtfUnknown;
namespace MediaBrowser.MediaEncoding.Subtitles
{
- public sealed class SubtitleEncoder : ISubtitleEncoder
+ public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
{
private readonly ILogger<SubtitleEncoder> _logger;
private readonly IApplicationPaths _appPaths;
@@ -40,8 +41,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary>
/// The _semaphoreLocks.
/// </summary>
- private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
- new ConcurrentDictionary<string, SemaphoreSlim>();
+ private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
public SubtitleEncoder(
ILogger<SubtitleEncoder> logger,
@@ -194,36 +198,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- string outputFormat;
- string outputCodec;
+ await ExtractAllTextSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
- if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
- || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
- || string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
- {
- // Extract
- outputCodec = "copy";
- outputFormat = subtitleStream.Codec;
- }
- else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase))
- {
- // Extract
- outputCodec = "copy";
- outputFormat = "srt";
- }
- else
- {
- // Extract
- outputCodec = "srt";
- outputFormat = "srt";
- }
-
- // Extract
+ var outputFormat = GetTextSubtitleFormat(subtitleStream);
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
- await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken)
- .ConfigureAwait(false);
-
return new SubtitleInfo()
{
Path = outputPath,
@@ -318,16 +297,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
/// <summary>
- /// Gets the lock.
- /// </summary>
- /// <param name="filename">The filename.</param>
- /// <returns>System.Object.</returns>
- private SemaphoreSlim GetLock(string filename)
- {
- return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1));
- }
-
- /// <summary>
/// Converts the text subtitle to SRT.
/// </summary>
/// <param name="subtitleStream">The subtitle stream.</param>
@@ -337,21 +306,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <returns>Task.</returns>
private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{
- var semaphore = GetLock(outputPath);
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(outputPath))
{
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
/// <summary>
@@ -467,6 +428,203 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
}
+ private string GetTextSubtitleFormat(MediaStream subtitleStream)
+ {
+ if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase))
+ {
+ return subtitleStream.Codec;
+ }
+ else
+ {
+ return "srt";
+ }
+ }
+
+ private bool IsCodecCopyable(string codec)
+ {
+ return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Extracts all text subtitles.
+ /// </summary>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken)
+ {
+ var locks = new List<AsyncKeyedLockReleaser<string>>();
+ var extractableStreams = new List<MediaStream>();
+
+ try
+ {
+ var subtitleStreams = mediaSource.MediaStreams
+ .Where(stream => stream.IsTextSubtitleStream && stream.SupportsExternalStream);
+
+ foreach (var subtitleStream in subtitleStreams)
+ {
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
+
+ var @lock = _semaphoreLocks.GetOrAdd(outputPath);
+ await @lock.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ if (File.Exists(outputPath))
+ {
+ @lock.Dispose();
+ continue;
+ }
+
+ locks.Add(@lock);
+ extractableStreams.Add(subtitleStream);
+ }
+
+ if (extractableStreams.Count > 0)
+ {
+ await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path);
+ }
+ finally
+ {
+ foreach (var @lock in locks)
+ {
+ @lock.Dispose();
+ }
+ }
+ }
+
+ private async Task ExtractAllTextSubtitlesInternal(
+ MediaSourceInfo mediaSource,
+ List<MediaStream> subtitleStreams,
+ CancellationToken cancellationToken)
+ {
+ var inputPath = mediaSource.Path;
+ var outputPaths = new List<string>();
+ var args = string.Format(
+ CultureInfo.InvariantCulture,
+ "-i {0} -copyts",
+ inputPath);
+
+ foreach (var subtitleStream in subtitleStreams)
+ {
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
+ var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
+ var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
+
+ if (streamIndex == -1)
+ {
+ _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index);
+ continue;
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
+
+ outputPaths.Add(outputPath);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0} -an -vn -c:s {1} \"{2}\"",
+ streamIndex,
+ outputCodec,
+ outputPath);
+ }
+
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = args,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
+ {
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ try
+ {
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting ffmpeg");
+
+ throw;
+ }
+
+ try
+ {
+ await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
+ }
+ }
+
+ var failed = false;
+
+ if (exitCode == -1)
+ {
+ failed = true;
+
+ foreach (var outputPath in outputPaths)
+ {
+ try
+ {
+ _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
+ _fileSystem.DeleteFile(outputPath);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
+ }
+ }
+ }
+ else
+ {
+ foreach (var outputPath in outputPaths)
+ {
+ if (!File.Exists(outputPath))
+ {
+ _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
+ failed = true;
+ continue;
+ }
+
+ if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase))
+ {
+ await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
+ }
+
+ _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
+ }
+ }
+
+ if (failed)
+ {
+ throw new FfmpegException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath));
+ }
+ }
+
/// <summary>
/// Extracts the text subtitle.
/// </summary>
@@ -484,16 +642,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string outputPath,
CancellationToken cancellationToken)
{
- var semaphore = GetLock(outputPath);
-
- await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
-
- try
+ using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
{
if (!File.Exists(outputPath))
{
+ var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
+
var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
if (subtitleStream.IsExternal)
@@ -509,10 +663,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- semaphore.Release();
- }
}
private async Task ExtractTextSubtitleInternal(
@@ -728,6 +878,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _semaphoreLocks.Dispose();
+ }
+
#pragma warning disable CA1034 // Nested types should not be visible
// Only public for the unit tests
public readonly record struct SubtitleInfo
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 483d0a1d82..ab3eb3298b 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -4,11 +4,14 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -42,7 +45,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
private readonly IAttachmentExtractor _attachmentExtractor;
private readonly List<TranscodingJob> _activeTranscodingJobs = new();
- private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new();
+ private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
+ {
+ o.PoolSize = 20;
+ o.PoolInitialFill = 1;
+ });
/// <summary>
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
@@ -223,11 +230,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(job.Path!);
- }
-
job.Stop();
if (delete(job.Path!))
@@ -400,7 +402,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
- var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+ var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
@@ -624,11 +626,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- lock (_transcodingLocks)
- {
- _transcodingLocks.Remove(path);
- }
-
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
@@ -704,21 +701,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
- /// <inheritdoc />
- public SemaphoreSlim GetTranscodingLock(string outputPath)
- {
- lock (_transcodingLocks)
- {
- if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
- {
- result = new SemaphoreSlim(1, 1);
- _transcodingLocks[outputPath] = result;
- }
-
- return result;
- }
- }
-
private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
@@ -741,10 +723,23 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
}
}
+ /// <summary>
+ /// Transcoding lock.
+ /// </summary>
+ /// <param name="outputPath">The output path of the transcoded file.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>An <see cref="IDisposable"/>.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
+ {
+ return _transcodingLocks.LockAsync(outputPath, cancellationToken);
+ }
+
/// <inheritdoc />
public void Dispose()
{
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
_sessionManager.PlaybackStart -= OnPlaybackProgress;
+ _transcodingLocks.Dispose();
}
}
diff --git a/MediaBrowser.Model/ClientLog/ClientLogEvent.cs b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs
deleted file mode 100644
index 21087b5647..0000000000
--- a/MediaBrowser.Model/ClientLog/ClientLogEvent.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using System;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Model.ClientLog
-{
- /// <summary>
- /// The client log event.
- /// </summary>
- public class ClientLogEvent
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="ClientLogEvent"/> class.
- /// </summary>
- /// <param name="timestamp">The log timestamp.</param>
- /// <param name="level">The log level.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="clientName">The client name.</param>
- /// <param name="clientVersion">The client version.</param>
- /// <param name="deviceId">The device id.</param>
- /// <param name="message">The message.</param>
- public ClientLogEvent(
- DateTime timestamp,
- LogLevel level,
- Guid? userId,
- string clientName,
- string clientVersion,
- string deviceId,
- string message)
- {
- Timestamp = timestamp;
- UserId = userId;
- ClientName = clientName;
- ClientVersion = clientVersion;
- DeviceId = deviceId;
- Message = message;
- Level = level;
- }
-
- /// <summary>
- /// Gets the event timestamp.
- /// </summary>
- public DateTime Timestamp { get; }
-
- /// <summary>
- /// Gets the log level.
- /// </summary>
- public LogLevel Level { get; }
-
- /// <summary>
- /// Gets the user id.
- /// </summary>
- public Guid? UserId { get; }
-
- /// <summary>
- /// Gets the client name.
- /// </summary>
- public string ClientName { get; }
-
- /// <summary>
- /// Gets the client version.
- /// </summary>
- public string ClientVersion { get; }
-
- ///
- /// <summary>
- /// Gets the device id.
- /// </summary>
- public string DeviceId { get; }
-
- /// <summary>
- /// Gets the log message.
- /// </summary>
- public string Message { get; }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
deleted file mode 100644
index 6625b79814..0000000000
--- a/MediaBrowser.Model/Dlna/DeviceIdentification.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Dlna
-{
- public class DeviceIdentification
- {
- /// <summary>
- /// Gets or sets the name of the friendly.
- /// </summary>
- /// <value>The name of the friendly.</value>
- public string FriendlyName { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the model number.
- /// </summary>
- /// <value>The model number.</value>
- public string ModelNumber { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the serial number.
- /// </summary>
- /// <value>The serial number.</value>
- public string SerialNumber { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the name of the model.
- /// </summary>
- /// <value>The name of the model.</value>
- public string ModelName { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the model description.
- /// </summary>
- /// <value>The model description.</value>
- public string ModelDescription { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the model URL.
- /// </summary>
- /// <value>The model URL.</value>
- public string ModelUrl { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the manufacturer.
- /// </summary>
- /// <value>The manufacturer.</value>
- public string Manufacturer { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the manufacturer URL.
- /// </summary>
- /// <value>The manufacturer URL.</value>
- public string ManufacturerUrl { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the headers.
- /// </summary>
- /// <value>The headers.</value>
- public HttpHeaderInfo[] Headers { get; set; } = Array.Empty<HttpHeaderInfo>();
- }
-}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 71d0896a70..2addebbfca 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,11 +1,7 @@
#pragma warning disable CA1819 // Properties should not return arrays
+
using System;
-using System.ComponentModel;
-using System.Linq;
using System.Xml.Serialization;
-using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
-using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
{
@@ -17,7 +13,6 @@ namespace MediaBrowser.Model.Dlna
/// the device is able to direct play (without transcoding or remuxing),
/// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
/// </summary>
- [XmlRoot("Profile")]
public class DeviceProfile
{
/// <summary>
@@ -32,104 +27,6 @@ namespace MediaBrowser.Model.Dlna
public string? Id { get; set; }
/// <summary>
- /// Gets or sets the Identification.
- /// </summary>
- public DeviceIdentification? Identification { get; set; }
-
- /// <summary>
- /// Gets or sets the friendly name of the device profile, which can be shown to users.
- /// </summary>
- public string? FriendlyName { get; set; }
-
- /// <summary>
- /// Gets or sets the manufacturer of the device which this profile represents.
- /// </summary>
- public string? Manufacturer { get; set; }
-
- /// <summary>
- /// Gets or sets an url for the manufacturer of the device which this profile represents.
- /// </summary>
- public string? ManufacturerUrl { get; set; }
-
- /// <summary>
- /// Gets or sets the model name of the device which this profile represents.
- /// </summary>
- public string? ModelName { get; set; }
-
- /// <summary>
- /// Gets or sets the model description of the device which this profile represents.
- /// </summary>
- public string? ModelDescription { get; set; }
-
- /// <summary>
- /// Gets or sets the model number of the device which this profile represents.
- /// </summary>
- public string? ModelNumber { get; set; }
-
- /// <summary>
- /// Gets or sets the ModelUrl.
- /// </summary>
- public string? ModelUrl { get; set; }
-
- /// <summary>
- /// Gets or sets the serial number of the device which this profile represents.
- /// </summary>
- public string? SerialNumber { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether EnableAlbumArtInDidl.
- /// </summary>
- [DefaultValue(false)]
- public bool EnableAlbumArtInDidl { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether EnableSingleAlbumArtLimit.
- /// </summary>
- [DefaultValue(false)]
- public bool EnableSingleAlbumArtLimit { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether EnableSingleSubtitleLimit.
- /// </summary>
- [DefaultValue(false)]
- public bool EnableSingleSubtitleLimit { get; set; }
-
- /// <summary>
- /// Gets or sets the SupportedMediaTypes.
- /// </summary>
- public string SupportedMediaTypes { get; set; } = "Audio,Photo,Video";
-
- /// <summary>
- /// Gets or sets the UserId.
- /// </summary>
- public string? UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the AlbumArtPn.
- /// </summary>
- public string? AlbumArtPn { get; set; }
-
- /// <summary>
- /// Gets or sets the MaxAlbumArtWidth.
- /// </summary>
- public int? MaxAlbumArtWidth { get; set; }
-
- /// <summary>
- /// Gets or sets the MaxAlbumArtHeight.
- /// </summary>
- public int? MaxAlbumArtHeight { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum allowed width of embedded icons.
- /// </summary>
- public int? MaxIconWidth { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum allowed height of embedded icons.
- /// </summary>
- public int? MaxIconHeight { get; set; }
-
- /// <summary>
/// Gets or sets the maximum allowed bitrate for all streamed content.
/// </summary>
public int? MaxStreamingBitrate { get; set; } = 8000000;
@@ -150,51 +47,6 @@ namespace MediaBrowser.Model.Dlna
public int? MaxStaticMusicBitrate { get; set; } = 8000000;
/// <summary>
- /// Gets or sets the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.
- /// </summary>
- public string? SonyAggregationFlags { get; set; }
-
- /// <summary>
- /// Gets or sets the ProtocolInfo.
- /// </summary>
- public string? ProtocolInfo { get; set; }
-
- /// <summary>
- /// Gets or sets the TimelineOffsetSeconds.
- /// </summary>
- [DefaultValue(0)]
- public int TimelineOffsetSeconds { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether RequiresPlainVideoItems.
- /// </summary>
- [DefaultValue(false)]
- public bool RequiresPlainVideoItems { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether RequiresPlainFolders.
- /// </summary>
- [DefaultValue(false)]
- public bool RequiresPlainFolders { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether EnableMSMediaReceiverRegistrar.
- /// </summary>
- [DefaultValue(false)]
- public bool EnableMSMediaReceiverRegistrar { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether IgnoreTranscodeByteRangeRequests.
- /// </summary>
- [DefaultValue(false)]
- public bool IgnoreTranscodeByteRangeRequests { get; set; }
-
- /// <summary>
- /// Gets or sets the XmlRootAttributes.
- /// </summary>
- public XmlAttribute[] XmlRootAttributes { get; set; } = Array.Empty<XmlAttribute>();
-
- /// <summary>
/// Gets or sets the direct play profiles.
/// </summary>
public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>();
@@ -215,297 +67,8 @@ namespace MediaBrowser.Model.Dlna
public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>();
/// <summary>
- /// Gets or sets the ResponseProfiles.
- /// </summary>
- public ResponseProfile[] ResponseProfiles { get; set; } = Array.Empty<ResponseProfile>();
-
- /// <summary>
/// Gets or sets the subtitle profiles.
/// </summary>
public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>();
-
- /// <summary>
- /// The GetSupportedMediaTypes.
- /// </summary>
- /// <returns>The .</returns>
- public MediaType[] GetSupportedMediaTypes()
- {
- return ContainerProfile.SplitValue(SupportedMediaTypes)
- .Select(m => Enum.TryParse<MediaType>(m, out var parsed) ? parsed : MediaType.Unknown)
- .Where(m => m != MediaType.Unknown)
- .ToArray();
- }
-
- /// <summary>
- /// Gets the audio transcoding profile.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="audioCodec">The audio Codec.</param>
- /// <returns>A <see cref="TranscodingProfile"/>.</returns>
- public TranscodingProfile? GetAudioTranscodingProfile(string? container, string? audioCodec)
- {
- container = (container ?? string.Empty).TrimStart('.');
-
- foreach (var i in TranscodingProfiles)
- {
- if (i.Type != DlnaProfileType.Audio)
- {
- continue;
- }
-
- if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- return i;
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the video transcoding profile.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="audioCodec">The audio Codec.</param>
- /// <param name="videoCodec">The video Codec.</param>
- /// <returns>The <see cref="TranscodingProfile"/>.</returns>
- public TranscodingProfile? GetVideoTranscodingProfile(string? container, string? audioCodec, string? videoCodec)
- {
- container = (container ?? string.Empty).TrimStart('.');
-
- foreach (var i in TranscodingProfiles)
- {
- if (i.Type != DlnaProfileType.Video)
- {
- continue;
- }
-
- if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- if (!string.Equals(videoCodec, i.VideoCodec, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- return i;
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the audio media profile.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="audioCodec">The audio codec.</param>
- /// <param name="audioChannels">The audio channels.</param>
- /// <param name="audioBitrate">The audio bitrate.</param>
- /// <param name="audioSampleRate">The audio sample rate.</param>
- /// <param name="audioBitDepth">The audio bit depth.</param>
- /// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile? GetAudioMediaProfile(string? container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
- {
- foreach (var i in ResponseProfiles)
- {
- if (i.Type != DlnaProfileType.Audio)
- {
- continue;
- }
-
- if (!ContainerProfile.ContainsContainer(i.GetContainers(), container))
- {
- continue;
- }
-
- var audioCodecs = i.GetAudioCodecs();
- if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var anyOff = false;
- foreach (ProfileCondition c in i.Conditions)
- {
- if (!ConditionProcessor.IsAudioConditionSatisfied(GetModelProfileCondition(c), audioChannels, audioBitrate, audioSampleRate, audioBitDepth))
- {
- anyOff = true;
- break;
- }
- }
-
- if (anyOff)
- {
- continue;
- }
-
- return i;
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the model profile condition.
- /// </summary>
- /// <param name="c">The c<see cref="ProfileCondition"/>.</param>
- /// <returns>The <see cref="ProfileCondition"/>.</returns>
- private ProfileCondition GetModelProfileCondition(ProfileCondition c)
- {
- return new ProfileCondition
- {
- Condition = c.Condition,
- IsRequired = c.IsRequired,
- Property = c.Property,
- Value = c.Value
- };
- }
-
- /// <summary>
- /// Gets the image media profile.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="width">The width.</param>
- /// <param name="height">The height.</param>
- /// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile? GetImageMediaProfile(string container, int? width, int? height)
- {
- foreach (var i in ResponseProfiles)
- {
- if (i.Type != DlnaProfileType.Photo)
- {
- continue;
- }
-
- if (!ContainerProfile.ContainsContainer(i.GetContainers(), container))
- {
- continue;
- }
-
- var anyOff = false;
- foreach (var c in i.Conditions)
- {
- if (!ConditionProcessor.IsImageConditionSatisfied(GetModelProfileCondition(c), width, height))
- {
- anyOff = true;
- break;
- }
- }
-
- if (anyOff)
- {
- continue;
- }
-
- return i;
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the video media profile.
- /// </summary>
- /// <param name="container">The container.</param>
- /// <param name="audioCodec">The audio codec.</param>
- /// <param name="videoCodec">The video codec.</param>
- /// <param name="width">The width.</param>
- /// <param name="height">The height.</param>
- /// <param name="bitDepth">The bit depth.</param>
- /// <param name="videoBitrate">The video bitrate.</param>
- /// <param name="videoProfile">The video profile.</param>
- /// <param name="videoRangeType">The video range type.</param>
- /// <param name="videoLevel">The video level.</param>
- /// <param name="videoFramerate">The video framerate.</param>
- /// <param name="packetLength">The packet length.</param>
- /// <param name="timestamp">The timestamp<see cref="TransportStreamTimestamp"/>.</param>
- /// <param name="isAnamorphic">True if anamorphic.</param>
- /// <param name="isInterlaced">True if interlaced.</param>
- /// <param name="refFrames">The ref frames.</param>
- /// <param name="numVideoStreams">The number of video streams.</param>
- /// <param name="numAudioStreams">The number of audio streams.</param>
- /// <param name="videoCodecTag">The video Codec tag.</param>
- /// <param name="isAvc">True if Avc.</param>
- /// <returns>The <see cref="ResponseProfile"/>.</returns>
- public ResponseProfile? GetVideoMediaProfile(
- string? container,
- string? audioCodec,
- string? videoCodec,
- int? width,
- int? height,
- int? bitDepth,
- int? videoBitrate,
- string? videoProfile,
- VideoRangeType videoRangeType,
- double? videoLevel,
- float? videoFramerate,
- int? packetLength,
- TransportStreamTimestamp timestamp,
- bool? isAnamorphic,
- bool? isInterlaced,
- int? refFrames,
- int? numVideoStreams,
- int? numAudioStreams,
- string? videoCodecTag,
- bool? isAvc)
- {
- foreach (var i in ResponseProfiles)
- {
- if (i.Type != DlnaProfileType.Video)
- {
- continue;
- }
-
- if (!ContainerProfile.ContainsContainer(i.GetContainers(), container))
- {
- continue;
- }
-
- var audioCodecs = i.GetAudioCodecs();
- if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var videoCodecs = i.GetVideoCodecs();
- if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var anyOff = false;
- foreach (ProfileCondition c in i.Conditions)
- {
- if (!ConditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
- {
- anyOff = true;
- break;
- }
- }
-
- if (anyOff)
- {
- continue;
- }
-
- return i;
- }
-
- return null;
- }
}
}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs
deleted file mode 100644
index 74c32c523e..0000000000
--- a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dlna
-{
- public class DeviceProfileInfo
- {
- /// <summary>
- /// Gets or sets the identifier.
- /// </summary>
- /// <value>The identifier.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the type.
- /// </summary>
- /// <value>The type.</value>
- public DeviceProfileType Type { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfileType.cs b/MediaBrowser.Model/Dlna/DeviceProfileType.cs
deleted file mode 100644
index 46062abd09..0000000000
--- a/MediaBrowser.Model/Dlna/DeviceProfileType.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dlna
-{
- public enum DeviceProfileType
- {
- System = 0,
- User = 1
- }
-}
diff --git a/MediaBrowser.Model/Dlna/HeaderMatchType.cs b/MediaBrowser.Model/Dlna/HeaderMatchType.cs
deleted file mode 100644
index 2a9abb20eb..0000000000
--- a/MediaBrowser.Model/Dlna/HeaderMatchType.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dlna
-{
- public enum HeaderMatchType
- {
- Equals = 0,
- Regex = 1,
- Substring = 2
- }
-}
diff --git a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs
deleted file mode 100644
index 17c4dffcc0..0000000000
--- a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System.Xml.Serialization;
-
-namespace MediaBrowser.Model.Dlna
-{
- public class HttpHeaderInfo
- {
- [XmlAttribute("name")]
- public string Name { get; set; }
-
- [XmlAttribute("value")]
- public string Value { get; set; }
-
- [XmlAttribute("match")]
- public HeaderMatchType Match { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs
deleted file mode 100644
index bf9661f7f3..0000000000
--- a/MediaBrowser.Model/Dlna/ResponseProfile.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using System.Xml.Serialization;
-
-namespace MediaBrowser.Model.Dlna
-{
- public class ResponseProfile
- {
- public ResponseProfile()
- {
- Conditions = Array.Empty<ProfileCondition>();
- }
-
- [XmlAttribute("container")]
- public string Container { get; set; }
-
- [XmlAttribute("audioCodec")]
- public string AudioCodec { get; set; }
-
- [XmlAttribute("videoCodec")]
- public string VideoCodec { get; set; }
-
- [XmlAttribute("type")]
- public DlnaProfileType Type { get; set; }
-
- [XmlAttribute("orgPn")]
- public string OrgPn { get; set; }
-
- [XmlAttribute("mimeType")]
- public string MimeType { get; set; }
-
- public ProfileCondition[] Conditions { get; set; }
-
- public string[] GetContainers()
- {
- return ContainerProfile.SplitValue(Container);
- }
-
- public string[] GetAudioCodecs()
- {
- return ContainerProfile.SplitValue(AudioCodec);
- }
-
- public string[] GetVideoCodecs()
- {
- return ContainerProfile.SplitValue(VideoCodec);
- }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index bf18d46dcd..e6b7f4d9b3 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
@@ -1536,7 +1537,7 @@ namespace MediaBrowser.Model.Dlna
private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
{
- if (options.ItemId.Equals(default))
+ if (options.ItemId.IsEmpty())
{
ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
}
@@ -1943,6 +1944,38 @@ namespace MediaBrowser.Model.Dlna
break;
}
+ case ProfileConditionValue.VideoCodecTag:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ continue;
+ }
+
+ // change from split by | to comma
+ // strip spaces to avoid having to encode
+ var values = value
+ .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "codectag", string.Join(',', values));
+ }
+ else if (condition.Condition == ProfileConditionType.EqualsAny)
+ {
+ var currentValue = item.GetOption(qualifier, "codectag");
+ if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue, StringComparison.OrdinalIgnoreCase)))
+ {
+ item.SetOption(qualifier, "codectag", currentValue);
+ }
+ else
+ {
+ item.SetOption(qualifier, "codectag", string.Join(',', values));
+ }
+ }
+
+ break;
+ }
+
case ProfileConditionValue.Height:
{
if (!enableNonQualifiedConditions)
diff --git a/MediaBrowser.Model/Dlna/XmlAttribute.cs b/MediaBrowser.Model/Dlna/XmlAttribute.cs
deleted file mode 100644
index 03bb2e4b11..0000000000
--- a/MediaBrowser.Model/Dlna/XmlAttribute.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System.Xml.Serialization;
-
-namespace MediaBrowser.Model.Dlna
-{
- /// <summary>
- /// Defines the <see cref="XmlAttribute" />.
- /// </summary>
- public class XmlAttribute
- {
- /// <summary>
- /// Gets or sets the name of the attribute.
- /// </summary>
- [XmlAttribute("name")]
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the value of the attribute.
- /// </summary>
- [XmlAttribute("value")]
- public string Value { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Dto/ImageByNameInfo.cs b/MediaBrowser.Model/Dto/ImageByNameInfo.cs
deleted file mode 100644
index 06cc3e73cf..0000000000
--- a/MediaBrowser.Model/Dto/ImageByNameInfo.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Dto
-{
- public class ImageByNameInfo
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
-
- /// <summary>
- /// Gets or sets the theme.
- /// </summary>
- /// <value>The theme.</value>
- public string Theme { get; set; }
-
- /// <summary>
- /// Gets or sets the context.
- /// </summary>
- /// <value>The context.</value>
- public string Context { get; set; }
-
- /// <summary>
- /// Gets or sets the length of the file.
- /// </summary>
- /// <value>The length of the file.</value>
- public long FileLength { get; set; }
-
- /// <summary>
- /// Gets or sets the format.
- /// </summary>
- /// <value>The format.</value>
- public string Format { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Entities/SpecialFolder.cs b/MediaBrowser.Model/Entities/SpecialFolder.cs
deleted file mode 100644
index 2250c5dffb..0000000000
--- a/MediaBrowser.Model/Entities/SpecialFolder.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Entities
-{
- public static class SpecialFolder
- {
- public const string TvShowSeries = "TvShowSeries";
- public const string TvGenres = "TvGenres";
- public const string TvGenre = "TvGenre";
- public const string TvLatest = "TvLatest";
- public const string TvNextUp = "TvNextUp";
- public const string TvResume = "TvResume";
- public const string TvFavoriteSeries = "TvFavoriteSeries";
- public const string TvFavoriteEpisodes = "TvFavoriteEpisodes";
-
- public const string MovieLatest = "MovieLatest";
- public const string MovieResume = "MovieResume";
- public const string MovieMovies = "MovieMovies";
- public const string MovieCollections = "MovieCollections";
- public const string MovieFavorites = "MovieFavorites";
- public const string MovieGenres = "MovieGenres";
- public const string MovieGenre = "MovieGenre";
-
- public const string MusicArtists = "MusicArtists";
- public const string MusicAlbumArtists = "MusicAlbumArtists";
- public const string MusicAlbums = "MusicAlbums";
- public const string MusicGenres = "MusicGenres";
- public const string MusicLatest = "MusicLatest";
- public const string MusicPlaylists = "MusicPlaylists";
- public const string MusicSongs = "MusicSongs";
- public const string MusicFavorites = "MusicFavorites";
- public const string MusicFavoriteArtists = "MusicFavoriteArtists";
- public const string MusicFavoriteAlbums = "MusicFavoriteAlbums";
- public const string MusicFavoriteSongs = "MusicFavoriteSongs";
- }
-}
diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs
index f900da5567..034a6bf8bf 100644
--- a/MediaBrowser.Model/IO/IStreamHelper.cs
+++ b/MediaBrowser.Model/IO/IStreamHelper.cs
@@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO
Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken);
- Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken);
-
Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs
deleted file mode 100644
index 80a6461957..0000000000
--- a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.LiveTv
-{
- public enum LiveTvTunerStatus
- {
- Available = 0,
- Disabled = 1,
- RecordingTv = 2,
- LiveTv = 3
- }
-}
diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs
deleted file mode 100644
index 1524786ea7..0000000000
--- a/MediaBrowser.Model/Net/SocketReceiveResult.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-#nullable disable
-
-using System.Net;
-
-namespace MediaBrowser.Model.Net
-{
- /// <summary>
- /// Used by the sockets wrapper to hold raw data received from a UDP socket.
- /// </summary>
- public sealed class SocketReceiveResult
- {
- /// <summary>
- /// Gets or sets the buffer to place received data into.
- /// </summary>
- public byte[] Buffer { get; set; }
-
- /// <summary>
- /// Gets or sets the number of bytes received.
- /// </summary>
- public int ReceivedBytes { get; set; }
-
- /// <summary>
- /// Gets or sets the <see cref="IPEndPoint"/> the data was received from.
- /// </summary>
- public IPEndPoint RemoteEndPoint { get; set; }
-
- /// <summary>
- /// Gets or sets the local <see cref="IPAddress"/>.
- /// </summary>
- public IPAddress LocalIPAddress { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index 597845fc17..5f51fb21c3 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
@@ -30,5 +31,15 @@ namespace MediaBrowser.Model.Session
public string AppStoreUrl { get; set; }
public string IconUrl { get; set; }
+
+ // TODO: Remove after 10.9
+ [Obsolete("Unused")]
+ [DefaultValue(false)]
+ public bool? SupportsContentUploading { get; set; }
+
+ // TODO: Remove after 10.9
+ [Obsolete("Unused")]
+ [DefaultValue(false)]
+ public bool? SupportsSync { get; set; }
}
}
diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs
index f5753467a1..058875cd3b 100644
--- a/MediaBrowser.Model/Session/HardwareEncodingType.cs
+++ b/MediaBrowser.Model/Session/HardwareEncodingType.cs
@@ -33,6 +33,11 @@
/// <summary>
/// Video ToolBox.
/// </summary>
- VideoToolBox = 5
+ VideoToolBox = 5,
+
+ /// <summary>
+ /// Rockchip Media Process Platform (RKMPP).
+ /// </summary>
+ RKMPP = 6
}
}
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index aa7c03ebd5..f37ac6a147 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -3,30 +3,12 @@
using System;
using System.Collections.Generic;
-using System.Runtime.InteropServices;
+using System.ComponentModel;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Model.System
{
/// <summary>
- /// Enum describing the location of the FFmpeg tool.
- /// </summary>
- public enum FFmpegLocation
- {
- /// <summary>No path to FFmpeg found.</summary>
- NotFound,
-
- /// <summary>Path supplied via command line using switch --ffmpeg.</summary>
- SetByArgument,
-
- /// <summary>User has supplied path via Transcoding UI page.</summary>
- Custom,
-
- /// <summary>FFmpeg tool found on system $PATH.</summary>
- System
- }
-
- /// <summary>
/// Class SystemInfo.
/// </summary>
public class SystemInfo : PublicSystemInfo
@@ -83,9 +65,11 @@ namespace MediaBrowser.Model.System
/// </summary>
/// <value><c>true</c>.</value>
[Obsolete("This is always true")]
+ [DefaultValue(true)]
public bool CanSelfRestart { get; set; } = true;
[Obsolete("This is always false")]
+ [DefaultValue(false)]
public bool CanLaunchWebBrowser { get; set; } = false;
/// <summary>
@@ -140,12 +124,15 @@ namespace MediaBrowser.Model.System
/// </summary>
/// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
[Obsolete("This should be handled by the package manager")]
+ [DefaultValue(false)]
public bool HasUpdateAvailable { get; set; }
[Obsolete("This isn't set correctly anymore")]
- public FFmpegLocation EncoderLocation { get; set; }
+ [DefaultValue("System")]
+ public string EncoderLocation { get; set; } = "System";
[Obsolete("This is no longer set")]
- public Architecture SystemArchitecture { get; set; } = Architecture.X64;
+ [DefaultValue("X64")]
+ public string SystemArchitecture { get; set; } = "X64";
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 4ba8844182..2e9547bf31 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -13,7 +13,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Configuration;
@@ -706,7 +705,7 @@ namespace MediaBrowser.Providers.Manager
{
BaseItem? referenceItem = null;
- if (!searchInfo.ItemId.Equals(default))
+ if (!searchInfo.ItemId.IsEmpty())
{
referenceItem = _libraryManager.GetItemById(searchInfo.ItemId);
}
@@ -944,7 +943,7 @@ namespace MediaBrowser.Providers.Manager
public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
{
ArgumentNullException.ThrowIfNull(itemId);
- if (itemId.Equals(default))
+ if (itemId.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(itemId));
}
@@ -1025,7 +1024,7 @@ namespace MediaBrowser.Providers.Manager
await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
break;
case Folder folder:
- await folder.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
+ await folder.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
break;
}
}
@@ -1036,7 +1035,7 @@ namespace MediaBrowser.Providers.Manager
{
await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
- await child.ValidateChildren(new SimpleProgress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
+ await child.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -1058,7 +1057,7 @@ namespace MediaBrowser.Providers.Manager
.Select(i => i.MusicArtist)
.Where(i => i is not null);
- var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), options, true, cancellationToken));
+ var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, cancellationToken));
await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs
deleted file mode 100644
index a6216ef305..0000000000
--- a/MediaBrowser.XbmcMetadata/EntryPoint.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.XbmcMetadata.Configuration;
-using MediaBrowser.XbmcMetadata.Savers;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.XbmcMetadata
-{
- public sealed class EntryPoint : IServerEntryPoint
- {
- private readonly IUserDataManager _userDataManager;
- private readonly ILogger<EntryPoint> _logger;
- private readonly IProviderManager _providerManager;
- private readonly IConfigurationManager _config;
-
- public EntryPoint(
- IUserDataManager userDataManager,
- ILogger<EntryPoint> logger,
- IProviderManager providerManager,
- IConfigurationManager config)
- {
- _userDataManager = userDataManager;
- _logger = logger;
- _providerManager = providerManager;
- _config = config;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _userDataManager.UserDataSaved += OnUserDataSaved;
-
- return Task.CompletedTask;
- }
-
- private void OnUserDataSaved(object? sender, UserDataSaveEventArgs e)
- {
- if (e.SaveReason == UserDataSaveReason.PlaybackFinished || e.SaveReason == UserDataSaveReason.TogglePlayed || e.SaveReason == UserDataSaveReason.UpdateUserRating)
- {
- if (!string.IsNullOrWhiteSpace(_config.GetNfoConfiguration().UserId))
- {
- _ = SaveMetadataForItemAsync(e.Item, ItemUpdateType.MetadataDownload);
- }
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- _userDataManager.UserDataSaved -= OnUserDataSaved;
- }
-
- private async Task SaveMetadataForItemAsync(BaseItem item, ItemUpdateType updateReason)
- {
- if (!item.IsFileProtocol || !item.SupportsLocalMetadata)
- {
- return;
- }
-
- try
- {
- await _providerManager.SaveMetadataAsync(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name);
- }
- }
- }
-}
diff --git a/MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs b/MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs
new file mode 100644
index 0000000000..b2882194dd
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.XbmcMetadata.Configuration;
+using MediaBrowser.XbmcMetadata.Savers;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.XbmcMetadata;
+
+/// <summary>
+/// <see cref="IHostedService"/> responsible for updating NFO files' user data.
+/// </summary>
+public sealed class NfoUserDataSaver : IHostedService
+{
+ private readonly ILogger<NfoUserDataSaver> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IUserDataManager _userDataManager;
+ private readonly IProviderManager _providerManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NfoUserDataSaver"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+ /// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
+ /// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
+ public NfoUserDataSaver(
+ ILogger<NfoUserDataSaver> logger,
+ IConfigurationManager config,
+ IUserDataManager userDataManager,
+ IProviderManager providerManager)
+ {
+ _logger = logger;
+ _config = config;
+ _userDataManager = userDataManager;
+ _providerManager = providerManager;
+ }
+
+ /// <inheritdoc />
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _userDataManager.UserDataSaved += OnUserDataSaved;
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _userDataManager.UserDataSaved -= OnUserDataSaved;
+ return Task.CompletedTask;
+ }
+
+ private async void OnUserDataSaved(object? sender, UserDataSaveEventArgs e)
+ {
+ if (e.SaveReason is not (UserDataSaveReason.PlaybackFinished
+ or UserDataSaveReason.TogglePlayed or UserDataSaveReason.UpdateUserRating))
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(_config.GetNfoConfiguration().UserId))
+ {
+ return;
+ }
+
+ var item = e.Item;
+ if (!item.IsFileProtocol || !item.SupportsLocalMetadata)
+ {
+ return;
+ }
+
+ try
+ {
+ await _providerManager.SaveMetadataAsync(item, ItemUpdateType.MetadataDownload, [BaseNfoSaver.SaverName])
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name);
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 70e5b66c1e..97cdc68545 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -460,10 +460,28 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var trailer = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(trailer))
{
- item.AddTrailerUrl(trailer.Replace(
- "plugin://plugin.video.youtube/?action=play_video&videoid=",
- BaseNfoSaver.YouTubeWatchUrl,
- StringComparison.OrdinalIgnoreCase));
+ if (trailer.StartsWith("plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase))
+ {
+ // Deprecated format
+ item.AddTrailerUrl(trailer.Replace(
+ "plugin://plugin.video.youtube/?action=play_video&videoid=",
+ BaseNfoSaver.YouTubeWatchUrl,
+ StringComparison.OrdinalIgnoreCase));
+
+ var suggestedUrl = trailer.Replace(
+ "plugin://plugin.video.youtube/?action=play_video&videoid=",
+ "plugin://plugin.video.youtube/play/?video_id=",
+ StringComparison.OrdinalIgnoreCase);
+ Logger.LogWarning("Trailer URL uses a deprecated format : {Url}. Using {NewUrl} instead is advised.", trailer, suggestedUrl);
+ }
+ else if (trailer.StartsWith("plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase))
+ {
+ // Proper format
+ item.AddTrailerUrl(trailer.Replace(
+ "plugin://plugin.video.youtube/play/?video_id=",
+ BaseNfoSaver.YouTubeWatchUrl,
+ StringComparison.OrdinalIgnoreCase));
+ }
}
break;
diff --git a/README.md b/README.md
index 62ef21334d..ec065f2603 100644
--- a/README.md
+++ b/README.md
@@ -142,17 +142,36 @@ cd Jellyfin.Server/bin/Debug/net8.0 # Change into the build output directory
2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`.
+#### Accessing the Hosted Web Client
+
+If the Server is configured to host the Web Client, and the Server is running, the Web Client can be accessed at `http://localhost:8096` by default.
+
+API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index.html`
+
+
### Running from GH-Codespaces
As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
-**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public.
-#### FFmpeg installation.
-Because sometimes you need FFMPEG to test certain cases, follow the instructions from the wiki on the dev enviorment:
-https://jellyfin.org/docs/general/installation/linux/#ffmpeg-installation
+**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 secounds to load all extensions and prepare the enviorment while vscode is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
+
+**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public.
**NOTE:** When first opening the server instance with any WebUI, you will be send to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
+There are two configurations for you to chose from.
+#### Default - Development Jellyfin Server
+This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run though the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` lunch config to start the server.
+
+> Keep in mind that as this has no web client you have to connect to it via an extenal client. This can be just another codespace container running the WebUI. vuejs does not work from the getgo as it does not support the setup steps.
+
+#### Development Jellyfin Server ffmpeg
+this extens the default server with an default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
+If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file.
+
+Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled.
+
+
### Running The Tests
This repository also includes unit tests that are used to validate functionality as part of a CI pipeline on Azure. There are several ways to run these tests.
diff --git a/bump_version b/bump_version
index 41d27f5c8a..dd55e62c79 100755
--- a/bump_version
+++ b/bump_version
@@ -21,7 +21,7 @@ fi
shared_version_file="./SharedVersion.cs"
build_file="./build.yaml"
# csproj files for nuget packages
-jellyfin_subprojects=(
+jellyfin_subprojects=(
MediaBrowser.Common/MediaBrowser.Common.csproj
Jellyfin.Data/Jellyfin.Data.csproj
MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -97,7 +97,7 @@ cat ${debian_changelog_file} >> ${debian_changelog_temp}
# Move into place
mv ${debian_changelog_temp} ${debian_changelog_file}
-# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting
+# 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 )"
diff --git a/debian/rules b/debian/rules
index 069d48aad1..79cd55a15d 100755
--- a/debian/rules
+++ b/debian/rules
@@ -7,27 +7,27 @@ HOST_ARCH := $(shell arch)
BUILD_ARCH := ${DEB_HOST_MULTIARCH}
ifeq ($(HOST_ARCH),x86_64)
# Building AMD64
- DOTNETRUNTIME := debian-x64
+ DOTNETRUNTIME := linux-x64
ifeq ($(BUILD_ARCH),arm-linux-gnueabihf)
# Cross-building ARM on AMD64
- DOTNETRUNTIME := debian-arm
+ DOTNETRUNTIME := linux-arm
endif
ifeq ($(BUILD_ARCH),aarch64-linux-gnu)
# Cross-building ARM on AMD64
- DOTNETRUNTIME := debian-arm64
+ DOTNETRUNTIME := linux-arm64
endif
endif
ifeq ($(HOST_ARCH),armv7l)
# Building ARM
- DOTNETRUNTIME := debian-arm
+ DOTNETRUNTIME := linux-arm
endif
ifeq ($(HOST_ARCH),arm64)
# Building ARM
- DOTNETRUNTIME := debian-arm64
+ DOTNETRUNTIME := linux-arm64
endif
ifeq ($(HOST_ARCH),aarch64)
# Building ARM
- DOTNETRUNTIME := debian-arm64
+ DOTNETRUNTIME := linux-arm64
endif
export DH_VERBOSE=1
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 7c9bbf39e9..af309b0831 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -1,29 +1,36 @@
-FROM centos:7
+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 yum update -yq \
- && yum install -yq epel-release \
- && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
+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/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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
+ && 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
+ && 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}/
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index d344c59646..da0c9dabd3 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -10,11 +14,14 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+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 \
- libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index 8a5411f059..6c4cb816f5 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,23 +15,26 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && 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 -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 9 \
- && cd cross-gcc-packages-amd64/cross-gcc-9-arm64 \
- && apt-get install -yqq --no-install-recommends \
- gcc-9-source libstdc++-9-dev-arm64-cross \
+ && 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-ust0:arm64 libstdc++-9-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
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index e95ba16962..b1fa6cee52 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,24 +15,27 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && 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 -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 9 \
- && cd cross-gcc-packages-amd64/cross-gcc-9-armhf \
- && apt-get install -yqq --no-install-recommends\
- gcc-9-source libstdc++-9-dev-armhf-cross \
+ && 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-ust0:armhf libstdc++-9-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
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
index 1749ca563c..ca16a08fbc 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -1,13 +1,12 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
+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
index bbddb61e4d..6e0f7d18e9 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -1,13 +1,12 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
+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
index 3de1d68878..44fb705e6d 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -1,13 +1,12 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
+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
index 66ead37d7d..75a6d1e649 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -1,7 +1,9 @@
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
@@ -9,21 +11,26 @@ 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 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/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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
-
+ && 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
+ && 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}/
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 386f7cefe0..6b8de3773f 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index 56c8773332..49d98da2ac 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index c9692c440a..aba33c8b23 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 2304615560..247f756150 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64
index 240d091869..a6e1ba217e 100644
--- a/deployment/Dockerfile.linux.musl-linux-arm64
+++ b/deployment/Dockerfile.linux.musl-linux-arm64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts unzip \
+ && apt-get install --no-install-recommends -yqq \
+ debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64
index 1b054dfc47..45980c363e 100644
--- a/deployment/Dockerfile.macos.amd64
+++ b/deployment/Dockerfile.macos.amd64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64
index 07e18da55a..ee3a813dde 100644
--- a/deployment/Dockerfile.macos.arm64
+++ b/deployment/Dockerfile.macos.arm64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -11,10 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 36135f7a6e..0ab1b19147 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -10,10 +14,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+ 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
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 84fa2028e5..2326d3e852 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -1,7 +1,11 @@
-FROM ubuntu:bionic
+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
@@ -11,16 +15,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && 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 libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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
+ 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
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index ca3aa35085..461a287a18 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -1,7 +1,11 @@
-FROM ubuntu:bionic
+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
@@ -11,39 +15,36 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg wget ca-certificates devscripts \
mmv build-essential lsb-release
-# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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
-
# 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 -yqq --no-install-recommends cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 6 \
- && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \
- && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
- && apt-get install -yqq --no-install-recommends \
- gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu \
- bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev \
+ && 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-ust0:arm64 libstdc++6:arm64 libssl-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
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index e52b7fba35..83fe32acf8 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -1,7 +1,11 @@
-FROM ubuntu:bionic
+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
@@ -11,39 +15,36 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg wget ca-certificates devscripts \
mmv build-essential lsb-release
-# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-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
-
# 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 -yqq cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 6 \
- && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \
- && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \
- && apt-get install -yqq --no-install-recommends \
- gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf \
- bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev \
+ && 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-ust0:armhf libstdc++6:armhf libssl-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
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index 08587aa7eb..358fb620ac 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -1,7 +1,11 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
+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
@@ -10,10 +14,13 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
- && apt-get install -yqq --no-install-recommends \
+ && apt-get install --no-install-recommends -yqq \
debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
- libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
+ 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
diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64
index a0ab93e4e0..26be377f10 100755
--- a/deployment/build.centos.amd64
+++ b/deployment/build.centos.amd64
@@ -1,6 +1,6 @@
#!/bin/bash
-#= CentOS/RHEL 7+ amd64 .rpm
+#= CentOS/RHEL 9+ amd64 .rpm
set -o errexit
set -o xtrace
@@ -10,7 +10,7 @@ pushd "${SOURCE_DIR}"
if [[ ${IS_DOCKER} == YES ]]; then
# Remove BuildRequires for dotnet, since it's installed manually
- pushd fedora
+ pushd centos
cp -a jellyfin.spec /tmp/spec.orig
sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
@@ -20,7 +20,7 @@ fi
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
- pushd fedora
+ 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 '#' )
@@ -35,23 +35,23 @@ EOF
fi
# Build RPM
-make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
-rm -f fedora/jellyfin*.tar.gz
+rm -f centos/jellyfin*.tar.gz
if [[ ${IS_DOCKER} == YES ]]; then
- pushd fedora
+ pushd centos
cp -a /tmp/spec.orig jellyfin.spec
- chown -Rc $(stat -c %u:%g "${ARTIFACT_DIR}") "${ARTIFACT_DIR}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
popd
fi
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index 1a59d02e91..350b22a854 100755
--- a/deployment/build.debian.amd64
+++ b/deployment/build.debian.amd64
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Debian 10+ amd64 .deb
+#= Debian 12+ amd64 .deb
set -o errexit
set -o xtrace
@@ -8,12 +8,6 @@ set -o xtrace
# Move to source directory
pushd "${SOURCE_DIR}"
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
- cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-8.0,/d' debian/control
-fi
-
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
@@ -37,7 +31,7 @@ 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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
index e1e30fab4a..0dfca0ab49 100755
--- a/deployment/build.debian.arm64
+++ b/deployment/build.debian.arm64
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Debian 10+ arm64 .deb
+#= Debian 12+ arm64 .deb
set -o errexit
set -o xtrace
@@ -8,12 +8,6 @@ set -o xtrace
# Move to source directory
pushd "${SOURCE_DIR}"
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
- cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-8.0,/d' debian/control
-fi
-
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
@@ -38,7 +32,7 @@ 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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
index e3e8ae004e..0ab9e2f9a4 100755
--- a/deployment/build.debian.armhf
+++ b/deployment/build.debian.armhf
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Debian 10+ arm64 .deb
+#= Debian 12+ arm64 .deb
set -o errexit
set -o xtrace
@@ -8,12 +8,6 @@ set -o xtrace
# Move to source directory
pushd "${SOURCE_DIR}"
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
- cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-8.0,/d' debian/control
-fi
-
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
@@ -38,7 +32,7 @@ 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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
index da345ec08a..2b4ec2a9c5 100755
--- a/deployment/build.fedora.amd64
+++ b/deployment/build.fedora.amd64
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Fedora 29+ amd64 .rpm
+#= Fedora 39+ amd64 .rpm
set -o errexit
set -o xtrace
@@ -42,7 +42,7 @@ rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
rm -f fedora/jellyfin*.tar.gz
@@ -51,7 +51,7 @@ if [[ ${IS_DOCKER} == YES ]]; then
pushd fedora
cp -a /tmp/spec.orig jellyfin.spec
- chown -Rc $(stat -c %u:%g "${ARTIFACT_DIR}") "${ARTIFACT_DIR}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
popd
fi
diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64
index c6baa61f6e..2998d2f9e3 100755
--- a/deployment/build.linux.amd64
+++ b/deployment/build.linux.amd64
@@ -16,16 +16,16 @@ else
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}
+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}"
+ 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
index 6523f8319a..0fa1764650 100755
--- a/deployment/build.linux.amd64-musl
+++ b/deployment/build.linux.amd64-musl
@@ -16,16 +16,16 @@ else
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}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64
index 6d6a8f803a..dc44ca330d 100755
--- a/deployment/build.linux.arm64
+++ b/deployment/build.linux.arm64
@@ -16,16 +16,16 @@ else
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}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf
index 5167dfcb8d..f9de9ff0a3 100755
--- a/deployment/build.linux.armhf
+++ b/deployment/build.linux.armhf
@@ -16,16 +16,16 @@ else
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}
+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}"
+ 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
index 57980314d4..ae9ab010f5 100755
--- a/deployment/build.linux.musl-linux-arm64
+++ b/deployment/build.linux.musl-linux-arm64
@@ -16,16 +16,16 @@ else
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}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.macos.amd64 b/deployment/build.macos.amd64
index c7711e82c7..81e0f43f62 100755
--- a/deployment/build.macos.amd64
+++ b/deployment/build.macos.amd64
@@ -16,16 +16,16 @@ else
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}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.macos.arm64 b/deployment/build.macos.arm64
index b07eaad4e0..0a6f37edea 100755
--- a/deployment/build.macos.arm64
+++ b/deployment/build.macos.arm64
@@ -16,16 +16,16 @@ else
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}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.portable b/deployment/build.portable
index ec151d2956..fad14fccfc 100755
--- a/deployment/build.portable
+++ b/deployment/build.portable
@@ -16,16 +16,16 @@ else
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}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
index 17968a6e93..6fd87a3aec 100755
--- a/deployment/build.ubuntu.amd64
+++ b/deployment/build.ubuntu.amd64
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Ubuntu 18.04+ amd64 .deb
+#= Ubuntu 22.04+ amd64 .deb
set -o errexit
set -o xtrace
@@ -8,12 +8,6 @@ set -o xtrace
# Move to source directory
pushd "${SOURCE_DIR}"
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
- cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-8.0,/d' debian/control
-fi
-
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
@@ -37,7 +31,7 @@ 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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
index ee7da9bb98..f783941c73 100755
--- a/deployment/build.ubuntu.arm64
+++ b/deployment/build.ubuntu.arm64
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Ubuntu 18.04+ arm64 .deb
+#= Ubuntu 22.04+ arm64 .deb
set -o errexit
set -o xtrace
@@ -8,12 +8,6 @@ set -o xtrace
# Move to source directory
pushd "${SOURCE_DIR}"
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
- cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-8.0,/d' debian/control
-fi
-
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
@@ -38,7 +32,7 @@ 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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
index 85c993282e..cde6708c5b 100755
--- a/deployment/build.ubuntu.armhf
+++ b/deployment/build.ubuntu.armhf
@@ -1,6 +1,6 @@
#!/bin/bash
-#= Ubuntu 18.04+ arm64 .deb
+#= Ubuntu 22.04+ arm64 .deb
set -o errexit
set -o xtrace
@@ -8,12 +8,6 @@ set -o xtrace
# Move to source directory
pushd "${SOURCE_DIR}"
-if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-8.0, since it's installed manually
- cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-8.0,/d' debian/control
-fi
-
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
@@ -38,7 +32,7 @@ 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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
index 20f976365d..cd07f4e0b2 100755
--- a/deployment/build.windows.amd64
+++ b/deployment/build.windows.amd64
@@ -23,30 +23,30 @@ 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
+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}
+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}
+zip -qr jellyfin-server_"${version}".portable.zip jellyfin-server_"${version}"
popd
-rm -rf ${output_dir}
+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}"
+ chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}"
fi
popd
diff --git a/fedora/README.md b/fedora/README.md
index d449b51c16..6ea87740f2 100644
--- a/fedora/README.md
+++ b/fedora/README.md
@@ -14,8 +14,10 @@ The RPM package for Fedora/CentOS requires some additional repositories as ffmpe
# ffmpeg from RPMfusion free
# Fedora
$ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
-# CentOS 7
-$ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.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
@@ -26,8 +28,10 @@ Jellyfin is build with `--self-contained` so no dotnet required for runtime.
# dotnet required for building the RPM
# Fedora
$ sudo dnf copr enable @dotnet-sig/dotnet
-# CentOS
-$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm
+# 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
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index fb9fb2f7da..5327495ad3 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -1,10 +1,4 @@
%global debug_package %{nil}
-# Set the dotnet runtime
-%if 0%{?fedora}
-%global dotnet_runtime fedora.%{fedora}-x64
-%else
-%global dotnet_runtime centos-x64
-%endif
Name: jellyfin
Version: 10.9.0
@@ -29,12 +23,6 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
BuildRequires: dotnet-runtime-8.0, dotnet-sdk-8.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
-# Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471
-%if 0%{?fedora} >= 36
-%global __requires_exclude ^liblttng-ust\\.so\\.0.*$
-%endif
-
-
%description
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
@@ -66,14 +54,14 @@ the Jellyfin server to bind to ports 80 and/or 443 for example.
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 %{dotnet_runtime} \
+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/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
+%{__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
diff --git a/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs
deleted file mode 100644
index 581fa000dc..0000000000
--- a/src/Jellyfin.Drawing.Skia/SkiaCodecException.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Globalization;
-using SkiaSharp;
-
-namespace Jellyfin.Drawing.Skia;
-
-/// <summary>
-/// Represents errors that occur during interaction with Skia codecs.
-/// </summary>
-public class SkiaCodecException : SkiaException
-{
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
- /// </summary>
- /// <param name="result">The non-successful codec result returned by Skia.</param>
- public SkiaCodecException(SKCodecResult result)
- {
- CodecResult = result;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
- /// with a specified error message.
- /// </summary>
- /// <param name="result">The non-successful codec result returned by Skia.</param>
- /// <param name="message">The message that describes the error.</param>
- public SkiaCodecException(SKCodecResult result, string message)
- : base(message)
- {
- CodecResult = result;
- }
-
- /// <summary>
- /// Gets the non-successful codec result returned by Skia.
- /// </summary>
- public SKCodecResult CodecResult { get; }
-
- /// <inheritdoc />
- public override string ToString()
- => string.Format(
- CultureInfo.InvariantCulture,
- "Non-success codec result: {0}\n{1}",
- CodecResult,
- base.ToString());
-}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 5721e28820..4ae5a9a483 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -182,7 +182,6 @@ public class SkiaEncoder : IImageEncoder
/// <inheritdoc />
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
- /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
public string GetImageBlurHash(int xComp, int yComp, string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
diff --git a/src/Jellyfin.Drawing.Skia/SkiaException.cs b/src/Jellyfin.Drawing.Skia/SkiaException.cs
deleted file mode 100644
index d0e69d42c8..0000000000
--- a/src/Jellyfin.Drawing.Skia/SkiaException.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-
-namespace Jellyfin.Drawing.Skia;
-
-/// <summary>
-/// Represents errors that occur during interaction with Skia.
-/// </summary>
-public class SkiaException : Exception
-{
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaException"/> class.
- /// </summary>
- public SkiaException()
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
- /// </summary>
- /// <param name="message">The message that describes the error.</param>
- public SkiaException(string message) : base(message)
- {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
- /// reference to the inner exception that is the cause of this exception.
- /// </summary>
- /// <param name="message">The error message that explains the reason for the exception.</param>
- /// <param name="innerException">
- /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
- /// no inner exception is specified.
- /// </param>
- public SkiaException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
-}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 65a8f4e832..213328a39f 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -7,6 +7,7 @@ using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -38,7 +39,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
- private readonly SemaphoreSlim _parallelEncodingLimit;
+ private readonly AsyncNonKeyedLocker _parallelEncodingLimit;
private bool _disposed;
@@ -68,7 +69,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
semaphoreCount = 2 * Environment.ProcessorCount;
}
- _parallelEncodingLimit = new(semaphoreCount, semaphoreCount);
+ _parallelEncodingLimit = new(semaphoreCount);
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
@@ -193,18 +194,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
{
if (!File.Exists(cacheFilePath))
{
- // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
- await _parallelEncodingLimit.WaitAsync().ConfigureAwait(false);
-
string resultPath;
- try
+
+ // Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
+ using (await _parallelEncodingLimit.LockAsync().ConfigureAwait(false))
{
resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
}
- finally
- {
- _parallelEncodingLimit.Release();
- }
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 23c4c0a9a4..4a02f90f95 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -21,4 +21,8 @@
<Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="AsyncKeyedLock" />
+ </ItemGroup>
+
</Project>
diff --git a/src/Jellyfin.Extensions/GuidExtensions.cs b/src/Jellyfin.Extensions/GuidExtensions.cs
new file mode 100644
index 0000000000..95c591a82e
--- /dev/null
+++ b/src/Jellyfin.Extensions/GuidExtensions.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jellyfin.Extensions;
+
+/// <summary>
+/// Guid specific extensions.
+/// </summary>
+public static class GuidExtensions
+{
+ /// <summary>
+ /// Determine whether the guid is default.
+ /// </summary>
+ /// <param name="guid">The guid.</param>
+ /// <returns>Whether the guid is the default value.</returns>
+ public static bool IsEmpty(this Guid guid)
+ => guid.Equals(default);
+
+ /// <summary>
+ /// Determine whether the guid is null or default.
+ /// </summary>
+ /// <param name="guid">The guid.</param>
+ /// <returns>Whether the guid is null or the default valueF.</returns>
+ public static bool IsNullOrEmpty([NotNullWhen(false)] this Guid? guid)
+ => guid is null || guid.Value.IsEmpty();
+}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
index 656e3c3dab..0a50b5c3b9 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Extensions.Json.Converters
{
// null got handled higher up the call stack
var val = value!.Value;
- if (val.Equals(default))
+ if (val.IsEmpty())
{
writer.WriteNullValue();
}
diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/src/Jellyfin.LiveTv/Channels/ChannelDynamicMediaSourceProvider.cs
index 3e149cc82c..839549ed67 100644
--- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelDynamicMediaSourceProvider.cs
@@ -8,7 +8,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
-namespace Emby.Server.Implementations.Channels
+namespace Jellyfin.LiveTv.Channels
{
/// <summary>
/// A media source provider for channels.
diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/src/Jellyfin.LiveTv/Channels/ChannelImageProvider.cs
index 25cbfcf146..32e224550f 100644
--- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelImageProvider.cs
@@ -7,7 +7,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-namespace Emby.Server.Implementations.Channels
+namespace Jellyfin.LiveTv.Channels
{
/// <summary>
/// An image provider for channels.
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index c505e8fe13..1948a9ab9b 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -8,12 +8,12 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using AsyncKeyedLock;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -34,7 +34,7 @@ using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Season = MediaBrowser.Controller.Entities.TV.Season;
using Series = MediaBrowser.Controller.Entities.TV.Series;
-namespace Emby.Server.Implementations.Channels
+namespace Jellyfin.LiveTv.Channels
{
/// <summary>
/// The LiveTV channel manager.
@@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IFileSystem _fileSystem;
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
- private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _resourcePool = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _disposed = false;
@@ -114,15 +114,6 @@ namespace Emby.Server.Implementations.Channels
}
/// <inheritdoc />
- public bool EnableMediaProbe(BaseItem item)
- {
- var internalChannel = _libraryManager.GetItemById(item.ChannelId);
- var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
-
- return channel is ISupportsMediaProbe;
- }
-
- /// <inheritdoc />
public Task DeleteItem(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
@@ -159,7 +150,7 @@ namespace Emby.Server.Implementations.Channels
/// <inheritdoc />
public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
{
- var user = query.UserId.Equals(default)
+ var user = query.UserId.IsEmpty()
? null
: _userManager.GetUserById(query.UserId);
@@ -272,7 +263,7 @@ namespace Emby.Server.Implementations.Channels
/// <inheritdoc />
public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
{
- var user = query.UserId.Equals(default)
+ var user = query.UserId.IsEmpty()
? null
: _userManager.GetUserById(query.UserId);
@@ -563,18 +554,6 @@ namespace Emby.Server.Implementations.Channels
}
/// <summary>
- /// Checks whether the provided Guid supports external transfer.
- /// </summary>
- /// <param name="channelId">The Guid.</param>
- /// <returns>Whether or not the provided Guid supports external transfer.</returns>
- public bool SupportsExternalTransfer(Guid channelId)
- {
- var channelProvider = GetChannelProvider(channelId);
-
- return channelProvider.GetChannelFeatures().SupportsContentDownloading;
- }
-
- /// <summary>
/// Gets the provided channel's supported features.
/// </summary>
/// <param name="channel">The channel.</param>
@@ -688,7 +667,7 @@ namespace Emby.Server.Implementations.Channels
ChannelIds = new Guid[] { internalChannel.Id }
};
- var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
+ var result = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).ConfigureAwait(false);
foreach (var item in result.Items)
{
@@ -701,7 +680,7 @@ namespace Emby.Server.Implementations.Channels
EnableTotalRecordCount = false,
ChannelIds = new Guid[] { internalChannel.Id }
},
- new SimpleProgress<double>(),
+ new Progress<double>(),
cancellationToken).ConfigureAwait(false);
}
}
@@ -716,7 +695,7 @@ namespace Emby.Server.Implementations.Channels
// Find the corresponding channel provider plugin
var channelProvider = GetChannelProvider(channel);
- var parentItem = query.ParentId.Equals(default)
+ var parentItem = query.ParentId.IsEmpty()
? channel
: _libraryManager.GetItemById(query.ParentId);
@@ -729,7 +708,7 @@ namespace Emby.Server.Implementations.Channels
cancellationToken)
.ConfigureAwait(false);
- if (query.ParentId.Equals(default))
+ if (query.ParentId.IsEmpty())
{
query.Parent = channel;
}
@@ -783,7 +762,7 @@ namespace Emby.Server.Implementations.Channels
/// <inheritdoc />
public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken)
{
- var internalResult = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
+ var internalResult = await GetChannelItemsInternal(query, new Progress<double>(), cancellationToken).ConfigureAwait(false);
var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User);
@@ -832,9 +811,7 @@ namespace Emby.Server.Implementations.Channels
{
}
- await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
try
{
@@ -881,10 +858,6 @@ namespace Emby.Server.Implementations.Channels
return result;
}
- finally
- {
- _resourcePool.Release();
- }
}
private async Task CacheResponse(ChannelItemResult result, string path)
@@ -1215,19 +1188,6 @@ namespace Emby.Server.Implementations.Channels
return result;
}
- internal IChannel GetChannelProvider(Guid internalChannelId)
- {
- var result = GetAllChannels()
- .FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name)));
-
- if (result is null)
- {
- throw new ResourceNotFoundException("No channel provider found for channel id " + internalChannelId);
- }
-
- return result;
- }
-
/// <inheritdoc />
public void Dispose()
{
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/src/Jellyfin.LiveTv/Channels/ChannelPostScanTask.cs
index b358ba4d55..b4f6cf731e 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelPostScanTask.cs
@@ -8,7 +8,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Channels
+namespace Jellyfin.LiveTv.Channels
{
/// <summary>
/// A task to remove all non-installed channels from the database.
diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
index cfd08e6535..79c5873d51 100644
--- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
@@ -2,14 +2,13 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.Channels
+namespace Jellyfin.LiveTv.Channels
{
/// <summary>
/// The "Refresh Channels" scheduled task.
@@ -66,7 +65,7 @@ namespace Emby.Server.Implementations.Channels
{
var manager = (ChannelManager)_channelManager;
- await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
+ await manager.RefreshChannels(new Progress<double>(), cancellationToken).ConfigureAwait(false);
await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken)
.ConfigureAwait(false);
diff --git a/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs
new file mode 100644
index 0000000000..67d0e5295b
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs
@@ -0,0 +1,18 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.LiveTv;
+
+namespace Jellyfin.LiveTv.Configuration;
+
+/// <summary>
+/// <see cref="IConfigurationManager"/> extensions for Live TV.
+/// </summary>
+public static class LiveTvConfigurationExtensions
+{
+ /// <summary>
+ /// Gets the <see cref="LiveTvOptions"/>.
+ /// </summary>
+ /// <param name="configurationManager">The <see cref="IConfigurationManager"/>.</param>
+ /// <returns>The <see cref="LiveTvOptions"/>.</returns>
+ public static LiveTvOptions GetLiveTvConfiguration(this IConfigurationManager configurationManager)
+ => configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+}
diff --git a/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs
new file mode 100644
index 0000000000..258afbb056
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.LiveTv;
+
+namespace Jellyfin.LiveTv.Configuration;
+
+/// <summary>
+/// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
+/// </summary>
+public class LiveTvConfigurationFactory : IConfigurationFactory
+{
+ /// <inheritdoc />
+ public IEnumerable<ConfigurationStore> GetConfigurations()
+ {
+ return new[]
+ {
+ new ConfigurationStore
+ {
+ ConfigurationType = typeof(LiveTvOptions),
+ Key = "livetv"
+ }
+ };
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/src/Jellyfin.LiveTv/EmbyTV/DirectRecorder.cs
index 7df66d3585..2a25218b63 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/DirectRecorder.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Streaming;
@@ -13,7 +12,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public sealed class DirectRecorder : IRecorder
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
index abe3ff349f..39f334184b 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
@@ -14,14 +14,13 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
-using Emby.Server.Implementations.Library;
+using AsyncKeyedLock;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
+using Jellyfin.LiveTv.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -39,15 +38,12 @@ using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public sealed class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
- private const int TunerDiscoveryDurationMs = 3000;
-
- private readonly IServerApplicationHost _appHost;
private readonly ILogger<EmbyTV> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
@@ -55,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
- private readonly LiveTvManager _liveTvManager;
+ private readonly ITunerHostManager _tunerHostManager;
private readonly IFileSystem _fileSystem;
private readonly ILibraryMonitor _libraryMonitor;
@@ -64,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IStreamHelper _streamHelper;
+ private readonly LiveTvDtoService _tvDtoService;
+ private readonly IListingsProvider[] _listingsProviders;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
@@ -71,27 +69,27 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels =
new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase);
- private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _recordingDeleteSemaphore = new(1);
private bool _disposed;
public EmbyTV(
- IServerApplicationHost appHost,
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
ILogger<EmbyTV> logger,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
- ILiveTvManager liveTvManager,
+ ITunerHostManager tunerHostManager,
IFileSystem fileSystem,
ILibraryManager libraryManager,
ILibraryMonitor libraryMonitor,
IProviderManager providerManager,
- IMediaEncoder mediaEncoder)
+ IMediaEncoder mediaEncoder,
+ LiveTvDtoService tvDtoService,
+ IEnumerable<IListingsProvider> listingsProviders)
{
Current = this;
- _appHost = appHost;
_logger = logger;
_httpClientFactory = httpClientFactory;
_config = config;
@@ -100,9 +98,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_libraryMonitor = libraryMonitor;
_providerManager = providerManager;
_mediaEncoder = mediaEncoder;
- _liveTvManager = (LiveTvManager)liveTvManager;
+ _tvDtoService = tvDtoService;
+ _tunerHostManager = tunerHostManager;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
+ _listingsProviders = listingsProviders.ToArray();
_seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
@@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
get
{
- var path = GetConfiguration().RecordingPath;
+ var path = _config.GetLiveTvConfiguration().RecordingPath;
return string.IsNullOrWhiteSpace(path)
? DefaultRecordingPath
@@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
pathsAdded.AddRange(pathsToCreate);
}
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
var pathsToRemove = config.MediaLocationsCreated
.Except(recordingFolders.SelectMany(i => i.Locations))
@@ -260,7 +260,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (requiresRefresh)
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
}
@@ -314,7 +314,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var list = new List<ChannelInfo>();
- foreach (var hostInstance in _liveTvManager.TunerHosts)
+ foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
@@ -514,7 +514,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var list = new List<ChannelInfo>();
- foreach (var hostInstance in _liveTvManager.TunerHosts)
+ foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
@@ -836,7 +836,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
{
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
var defaults = new SeriesTimerInfo()
{
@@ -937,10 +937,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
{
- return GetConfiguration().ListingProviders
+ return _config.GetLiveTvConfiguration().ListingProviders
.Select(i =>
{
- var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+ var provider = _listingsProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase));
return provider is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i);
})
@@ -970,7 +970,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return result;
}
- foreach (var hostInstance in _liveTvManager.TunerHosts)
+ foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
@@ -1002,7 +1002,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(channelId));
}
- foreach (var hostInstance in _liveTvManager.TunerHosts)
+ foreach (var hostInstance in _tunerHostManager.TunerHosts)
{
try
{
@@ -1021,45 +1021,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
- public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
- {
- var stream = new MediaSourceInfo
- {
- EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
- EncoderProtocol = MediaProtocol.Http,
- Path = info.Path,
- Protocol = MediaProtocol.File,
- Id = info.Id,
- SupportsDirectPlay = false,
- SupportsDirectStream = true,
- SupportsTranscoding = true,
- IsInfiniteStream = true,
- RequiresOpening = false,
- RequiresClosing = false,
- BufferMs = 0,
- IgnoreDts = true,
- IgnoreIndex = true
- };
-
- await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths)
- .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
-
- return new List<MediaSourceInfo>
- {
- stream
- };
- }
-
public Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
- public Task RecordLiveStream(string id, CancellationToken cancellationToken)
- {
- return Task.CompletedTask;
- }
-
public Task ResetTuner(string id, CancellationToken cancellationToken)
{
return Task.CompletedTask;
@@ -1110,7 +1076,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private string GetRecordingPath(TimerInfo timer, RemoteSearchResult metadata, out string seriesPath)
{
var recordPath = RecordingPath;
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
seriesPath = null;
if (timer.IsProgramSeries)
@@ -1218,6 +1184,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return Path.Combine(recordPath, recordingFileName);
}
+ private BaseItem GetLiveTvChannel(TimerInfo timer)
+ {
+ var internalChannelId = _tvDtoService.GetInternalChannelId(Name, timer.ChannelId);
+ return _libraryManager.GetItemById(internalChannelId);
+ }
+
private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo)
{
ArgumentNullException.ThrowIfNull(timer);
@@ -1243,7 +1215,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false);
var recordPath = GetRecordingPath(timer, remoteMetadata, out string seriesPath);
- var channelItem = _liveTvManager.GetLiveTvChannel(timer, this);
+ var channelItem = GetLiveTvChannel(timer);
string liveStreamId = null;
RecordingStatus recordingStatus;
@@ -1481,9 +1453,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
-
- try
+ using (await _recordingDeleteSemaphore.LockAsync().ConfigureAwait(false))
{
if (_disposed)
{
@@ -1536,10 +1506,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
}
- finally
- {
- _recordingDeleteSemaphore.Release();
- }
}
private void DeleteLibraryItemsForTimers(List<TimerInfo> timers)
@@ -1630,7 +1596,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void PostProcessRecording(TimerInfo timer, string path)
{
- var options = GetConfiguration();
+ var options = _config.GetLiveTvConfiguration();
if (string.IsNullOrWhiteSpace(options.RecordingPostProcessor))
{
return;
@@ -1811,7 +1777,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
program.AddGenre("News");
}
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
if (config.SaveRecordingNFO)
{
@@ -2029,7 +1995,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false);
}
- var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
+ var people = item.Id.IsEmpty() ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
var directors = people
.Where(i => i.IsType(PersonKind.Director))
@@ -2126,7 +2092,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var query = new InternalItemsQuery
{
- ItemIds = new[] { _liveTvManager.GetInternalProgramId(programId) },
+ ItemIds = [_tvDtoService.GetInternalProgramId(programId)],
Limit = 1,
DtoOptions = new DtoOptions()
};
@@ -2156,17 +2122,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!string.IsNullOrWhiteSpace(channelId))
{
- query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, channelId) };
+ query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, channelId)];
}
return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault();
}
- private LiveTvOptions GetConfiguration()
- {
- return _config.GetConfiguration<LiveTvOptions>("livetv");
- }
-
private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
{
if (timer.IsManual)
@@ -2197,7 +2158,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void HandleDuplicateShowIds(List<TimerInfo> timers)
{
// sort showings by HD channels first, then by startDate, record earliest showing possible
- foreach (var timer in timers.OrderByDescending(t => _liveTvManager.GetLiveTvChannel(t, this).IsHD).ThenBy(t => t.StartDate).Skip(1))
+ foreach (var timer in timers.OrderByDescending(t => GetLiveTvChannel(t).IsHD).ThenBy(t => t.StartDate).Skip(1))
{
timer.Status = RecordingStatus.Cancelled;
_timerProvider.Update(timer);
@@ -2347,7 +2308,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!seriesTimer.RecordAnyChannel)
{
- query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, seriesTimer.ChannelId) };
+ query.ChannelIds = [_tvDtoService.GetInternalChannelId(Name, seriesTimer.ChannelId)];
}
var tempChannelCache = new Dictionary<Guid, LiveTvChannel>();
@@ -2359,7 +2320,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId;
- if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(default))
+ if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.IsEmpty())
{
if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel))
{
@@ -2418,7 +2379,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
string channelId = null;
- if (!programInfo.ChannelId.Equals(default))
+ if (!programInfo.ChannelId.IsEmpty())
{
if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel))
{
@@ -2553,7 +2514,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
};
}
- var customPath = GetConfiguration().MovieRecordingPath;
+ var customPath = _config.GetLiveTvConfiguration().MovieRecordingPath;
if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
yield return new VirtualFolderInfo
@@ -2564,7 +2525,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
};
}
- customPath = GetConfiguration().SeriesRecordingPath;
+ customPath = _config.GetLiveTvConfiguration().SeriesRecordingPath;
if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath))
{
yield return new VirtualFolderInfo
@@ -2575,81 +2536,5 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
};
}
}
-
- public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
- {
- var list = new List<TunerHostInfo>();
-
- var configuredDeviceIds = GetConfiguration().TunerHosts
- .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
- .Select(i => i.DeviceId)
- .ToList();
-
- foreach (var host in _liveTvManager.TunerHosts)
- {
- var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
-
- if (newDevicesOnly)
- {
- discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase))
- .ToList();
- }
-
- list.AddRange(discoveredDevices);
- }
-
- return list;
- }
-
- public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
- {
- foreach (var host in _liveTvManager.TunerHosts)
- {
- await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
- }
- }
-
- private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
- {
- var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
-
- var configuredDevices = GetConfiguration().TunerHosts
- .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
- .ToList();
-
- foreach (var device in discoveredDevices)
- {
- var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
-
- if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url);
-
- configuredDevice.Url = device.Url;
- await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false);
- }
- }
- }
-
- private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
- {
- try
- {
- var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
-
- foreach (var device in discoveredDevices)
- {
- _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url);
- }
-
- return discoveredDevices;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error discovering tuner devices");
-
- return new List<TunerHostInfo>();
- }
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/src/Jellyfin.LiveTv/EmbyTV/EncodedRecorder.cs
index 9a9fd02739..132a5fc516 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/EncodedRecorder.cs
@@ -23,7 +23,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public class EncodedRecorder : IRecorder
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/src/Jellyfin.LiveTv/EmbyTV/EpgChannelData.cs
index 20a8213a77..43d308c434 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/EpgChannelData.cs
@@ -4,7 +4,7 @@ using System;
using System.Collections.Generic;
using MediaBrowser.Controller.LiveTv;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
internal class EpgChannelData
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/src/Jellyfin.LiveTv/EmbyTV/IRecorder.cs
index de14d6d086..7ed42e2634 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/IRecorder.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public interface IRecorder : IDisposable
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs
index d5a6feb470..547ffeb668 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs
@@ -9,7 +9,7 @@ using System.Text.Json;
using Jellyfin.Extensions.Json;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public class ItemDataProvider<T>
where T : class
diff --git a/src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs b/src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs
new file mode 100644
index 0000000000..dc15d53ffa
--- /dev/null
+++ b/src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.LiveTv;
+using Microsoft.Extensions.Hosting;
+
+namespace Jellyfin.LiveTv.EmbyTV;
+
+/// <summary>
+/// <see cref="IHostedService"/> responsible for initializing Live TV.
+/// </summary>
+public sealed class LiveTvHost : IHostedService
+{
+ private readonly EmbyTV _service;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LiveTvHost"/> class.
+ /// </summary>
+ /// <param name="services">The available <see cref="ILiveTvService"/>s.</param>
+ public LiveTvHost(IEnumerable<ILiveTvService> services)
+ {
+ _service = services.OfType<EmbyTV>().First();
+ }
+
+ /// <inheritdoc />
+ public Task StartAsync(CancellationToken cancellationToken) => _service.Start();
+
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/src/Jellyfin.LiveTv/EmbyTV/NfoConfigurationExtensions.cs
index 83f5e84131..e8570f0e0d 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/NfoConfigurationExtensions.cs
@@ -1,7 +1,7 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
/// <summary>
/// Class containing extension methods for working with the nfo configuration.
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/src/Jellyfin.LiveTv/EmbyTV/RecordingHelper.cs
index 7bbeae866a..6bda231b24 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/RecordingHelper.cs
@@ -5,7 +5,7 @@ using System.Globalization;
using System.Text;
using MediaBrowser.Controller.LiveTv;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
internal static class RecordingHelper
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs
index bf28f3b67d..2ebe60b296 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs
@@ -4,7 +4,7 @@ using System;
using MediaBrowser.Controller.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs
index 9f8441fa48..37b1fa14cc 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs
@@ -10,7 +10,7 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.EmbyTV
+namespace Jellyfin.LiveTv.EmbyTV
{
public class TimerManager : ItemDataProvider<TimerInfo>
{
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/src/Jellyfin.LiveTv/ExclusiveLiveStream.cs
index b1649afad5..9d442e20cc 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/src/Jellyfin.LiveTv/ExclusiveLiveStream.cs
@@ -1,5 +1,6 @@
#nullable disable
+#pragma warning disable CA1711
#pragma warning disable CS1591
using System;
@@ -10,7 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
-namespace Emby.Server.Implementations.Library
+namespace Jellyfin.LiveTv
{
public sealed class ExclusiveLiveStream : ILiveStream
{
diff --git a/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..a07325ad18
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs
@@ -0,0 +1,37 @@
+using Jellyfin.LiveTv.Channels;
+using Jellyfin.LiveTv.Guide;
+using Jellyfin.LiveTv.Listings;
+using Jellyfin.LiveTv.TunerHosts;
+using Jellyfin.LiveTv.TunerHosts.HdHomerun;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Jellyfin.LiveTv.Extensions;
+
+/// <summary>
+/// Live TV extensions for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class LiveTvServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds Live TV services to the <see cref="IServiceCollection"/>.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
+ public static void AddLiveTvServices(this IServiceCollection services)
+ {
+ services.AddSingleton<LiveTvDtoService>();
+ services.AddSingleton<ILiveTvManager, LiveTvManager>();
+ services.AddSingleton<IChannelManager, ChannelManager>();
+ services.AddSingleton<IStreamHelper, StreamHelper>();
+ services.AddSingleton<ITunerHostManager, TunerHostManager>();
+ services.AddSingleton<IGuideManager, GuideManager>();
+
+ services.AddSingleton<ILiveTvService, EmbyTV.EmbyTV>();
+ services.AddSingleton<ITunerHost, HdHomerunHost>();
+ services.AddSingleton<ITunerHost, M3UTunerHost>();
+ services.AddSingleton<IListingsProvider, SchedulesDirect>();
+ services.AddSingleton<IListingsProvider, XmlTvListingsProvider>();
+ }
+}
diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
new file mode 100644
index 0000000000..394fbbaeab
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
@@ -0,0 +1,707 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using Jellyfin.LiveTv.Configuration;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.LiveTv;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.LiveTv.Guide;
+
+/// <inheritdoc />
+public class GuideManager : IGuideManager
+{
+ private const int MaxGuideDays = 14;
+ private const string EtagKey = "ProgramEtag";
+ private const string ExternalServiceTag = "ExternalServiceId";
+
+ private readonly ILogger<GuideManager> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IItemRepository _itemRepo;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly ITunerHostManager _tunerHostManager;
+ private readonly LiveTvDtoService _tvDtoService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GuideManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
+ /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+ /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
+ /// <param name="itemRepo">The <see cref="IItemRepository"/>.</param>
+ /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
+ /// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
+ /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
+ /// <param name="tvDtoService">The <see cref="LiveTvDtoService"/>.</param>
+ public GuideManager(
+ ILogger<GuideManager> logger,
+ IConfigurationManager config,
+ IFileSystem fileSystem,
+ IItemRepository itemRepo,
+ ILibraryManager libraryManager,
+ ILiveTvManager liveTvManager,
+ ITunerHostManager tunerHostManager,
+ LiveTvDtoService tvDtoService)
+ {
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ _itemRepo = itemRepo;
+ _libraryManager = libraryManager;
+ _liveTvManager = liveTvManager;
+ _tunerHostManager = tunerHostManager;
+ _tvDtoService = tvDtoService;
+ }
+
+ /// <inheritdoc />
+ public GuideInfo GetGuideInfo()
+ {
+ var startDate = DateTime.UtcNow;
+ var endDate = startDate.AddDays(GetGuideDays());
+
+ return new GuideInfo
+ {
+ StartDate = startDate,
+ EndDate = endDate
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task RefreshGuide(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(progress);
+
+ await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false);
+
+ await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
+
+ var numComplete = 0;
+ double progressPerService = _liveTvManager.Services.Count == 0
+ ? 0
+ : 1.0 / _liveTvManager.Services.Count;
+
+ var newChannelIdList = new List<Guid>();
+ var newProgramIdList = new List<Guid>();
+
+ var cleanDatabase = true;
+
+ foreach (var service in _liveTvManager.Services)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _logger.LogDebug("Refreshing guide from {Name}", service.Name);
+
+ try
+ {
+ var innerProgress = new Progress<double>(p => progress.Report(p * progressPerService));
+
+ var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false);
+
+ newChannelIdList.AddRange(idList.Item1);
+ newProgramIdList.AddRange(idList.Item2);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ cleanDatabase = false;
+ _logger.LogError(ex, "Error refreshing channels for service");
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= _liveTvManager.Services.Count;
+
+ progress.Report(100 * percent);
+ }
+
+ if (cleanDatabase)
+ {
+ CleanDatabase(newChannelIdList.ToArray(), [BaseItemKind.LiveTvChannel], progress, cancellationToken);
+ CleanDatabase(newProgramIdList.ToArray(), [BaseItemKind.LiveTvProgram], progress, cancellationToken);
+ }
+
+ var coreService = _liveTvManager.Services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
+ if (coreService is not null)
+ {
+ await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
+ await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
+ }
+
+ progress.Report(100);
+ }
+
+ private double GetGuideDays()
+ {
+ var config = _config.GetLiveTvConfiguration();
+
+ return config.GuideDays.HasValue
+ ? Math.Clamp(config.GuideDays.Value, 1, MaxGuideDays)
+ : 7;
+ }
+
+ private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ progress.Report(10);
+
+ var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
+ .Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
+ .ToList();
+
+ var list = new List<LiveTvChannel>();
+
+ var numComplete = 0;
+ var parentFolder = _liveTvManager.GetInternalLiveTvFolder(cancellationToken);
+
+ foreach (var channelInfo in allChannelsList)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false);
+
+ list.Add(item);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= allChannelsList.Count;
+
+ progress.Report((5 * percent) + 10);
+ }
+
+ progress.Report(15);
+
+ numComplete = 0;
+ var programs = new List<Guid>();
+ var channels = new List<Guid>();
+
+ var guideDays = GetGuideDays();
+
+ _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
+
+ foreach (var currentChannel in list)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ channels.Add(currentChannel.Id);
+
+ try
+ {
+ var start = DateTime.UtcNow.AddHours(-1);
+ var end = start.AddDays(guideDays);
+
+ var isMovie = false;
+ var isSports = false;
+ var isNews = false;
+ var isKids = false;
+ var isSeries = false;
+
+ var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false)).ToList();
+
+ var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.LiveTvProgram],
+ ChannelIds = [currentChannel.Id],
+ DtoOptions = new DtoOptions(true)
+ }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
+
+ var newPrograms = new List<LiveTvProgram>();
+ var updatedPrograms = new List<BaseItem>();
+
+ foreach (var program in channelPrograms)
+ {
+ var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
+ if (isNew)
+ {
+ newPrograms.Add(programItem);
+ }
+ else if (isUpdated)
+ {
+ updatedPrograms.Add(programItem);
+ }
+
+ programs.Add(programItem.Id);
+
+ isMovie |= program.IsMovie;
+ isSeries |= program.IsSeries;
+ isSports |= program.IsSports;
+ isNews |= program.IsNews;
+ isKids |= program.IsKids;
+ }
+
+ _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
+
+ if (newPrograms.Count > 0)
+ {
+ _libraryManager.CreateItems(newPrograms, null, cancellationToken);
+ }
+
+ if (updatedPrograms.Count > 0)
+ {
+ await _libraryManager.UpdateItemsAsync(
+ updatedPrograms,
+ currentChannel,
+ ItemUpdateType.MetadataImport,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ currentChannel.IsMovie = isMovie;
+ currentChannel.IsNews = isNews;
+ currentChannel.IsSports = isSports;
+ currentChannel.IsSeries = isSeries;
+
+ if (isKids)
+ {
+ currentChannel.AddTag("Kids");
+ }
+
+ await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ await currentChannel.RefreshMetadata(
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
+ }
+
+ numComplete++;
+ double percent = numComplete / (double)allChannelsList.Count;
+
+ progress.Report((85 * percent) + 15);
+ }
+
+ progress.Report(100);
+ return new Tuple<List<Guid>, List<Guid>>(channels, programs);
+ }
+
+ private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
+ {
+ IncludeItemTypes = validTypes,
+ DtoOptions = new DtoOptions(false)
+ });
+
+ var numComplete = 0;
+
+ foreach (var itemId in list)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (itemId.IsEmpty())
+ {
+ // Somehow some invalid data got into the db. It probably predates the boundary checking
+ continue;
+ }
+
+ if (!currentIdList.Contains(itemId))
+ {
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item is not null)
+ {
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false,
+ DeleteFromExternalProvider = false
+ },
+ false);
+ }
+ }
+
+ numComplete++;
+ double percent = numComplete / (double)list.Count;
+
+ progress.Report(100 * percent);
+ }
+ }
+
+ private async Task<LiveTvChannel> GetChannel(
+ ChannelInfo channelInfo,
+ string serviceName,
+ BaseItem parentFolder,
+ CancellationToken cancellationToken)
+ {
+ var parentFolderId = parentFolder.Id;
+ var isNew = false;
+ var forceUpdate = false;
+
+ var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id);
+
+ if (_libraryManager.GetItemById(id) is not LiveTvChannel item)
+ {
+ item = new LiveTvChannel
+ {
+ Name = channelInfo.Name,
+ Id = id,
+ DateCreated = DateTime.UtcNow
+ };
+
+ isNew = true;
+ }
+
+ if (channelInfo.Tags is not null)
+ {
+ if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase))
+ {
+ isNew = true;
+ }
+
+ item.Tags = channelInfo.Tags;
+ }
+
+ if (!item.ParentId.Equals(parentFolderId))
+ {
+ isNew = true;
+ }
+
+ item.ParentId = parentFolderId;
+
+ item.ChannelType = channelInfo.ChannelType;
+ item.ServiceName = serviceName;
+
+ if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase))
+ {
+ forceUpdate = true;
+ }
+
+ item.SetProviderId(ExternalServiceTag, serviceName);
+
+ if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
+ {
+ forceUpdate = true;
+ }
+
+ item.ExternalId = channelInfo.Id;
+
+ if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
+ {
+ forceUpdate = true;
+ }
+
+ item.Number = channelInfo.Number;
+
+ if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
+ {
+ forceUpdate = true;
+ }
+
+ item.Name = channelInfo.Name;
+
+ if (!item.HasImage(ImageType.Primary))
+ {
+ if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
+ {
+ item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
+ forceUpdate = true;
+ }
+ else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl))
+ {
+ item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
+ forceUpdate = true;
+ }
+ }
+
+ if (isNew)
+ {
+ _libraryManager.CreateItem(item, parentFolder);
+ }
+ else if (forceUpdate)
+ {
+ await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ }
+
+ return item;
+ }
+
+ private (LiveTvProgram Item, bool IsNew, bool IsUpdated) GetProgram(
+ ProgramInfo info,
+ Dictionary<Guid, LiveTvProgram> allExistingPrograms,
+ LiveTvChannel channel)
+ {
+ var id = _tvDtoService.GetInternalProgramId(info.Id);
+
+ var isNew = false;
+ var forceUpdate = false;
+
+ if (!allExistingPrograms.TryGetValue(id, out var item))
+ {
+ isNew = true;
+ item = new LiveTvProgram
+ {
+ Name = info.Name,
+ Id = id,
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow
+ };
+
+ if (!string.IsNullOrEmpty(info.Etag))
+ {
+ item.SetProviderId(EtagKey, info.Etag);
+ }
+ }
+
+ if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
+ {
+ item.ShowId = info.ShowId;
+ forceUpdate = true;
+ }
+
+ var seriesId = info.SeriesId;
+
+ if (!item.ParentId.Equals(channel.Id))
+ {
+ forceUpdate = true;
+ }
+
+ item.ParentId = channel.Id;
+
+ item.Audio = info.Audio;
+ item.ChannelId = channel.Id;
+ item.CommunityRating ??= info.CommunityRating;
+ if ((item.CommunityRating ?? 0).Equals(0))
+ {
+ item.CommunityRating = null;
+ }
+
+ item.EpisodeTitle = info.EpisodeTitle;
+ item.ExternalId = info.Id;
+
+ if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
+ {
+ forceUpdate = true;
+ }
+
+ item.ExternalSeriesId = seriesId;
+
+ var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
+
+ if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
+ {
+ item.SeriesName = info.Name;
+ }
+
+ var tags = new List<string>();
+ if (info.IsLive)
+ {
+ tags.Add("Live");
+ }
+
+ if (info.IsPremiere)
+ {
+ tags.Add("Premiere");
+ }
+
+ if (info.IsNews)
+ {
+ tags.Add("News");
+ }
+
+ if (info.IsSports)
+ {
+ tags.Add("Sports");
+ }
+
+ if (info.IsKids)
+ {
+ tags.Add("Kids");
+ }
+
+ if (info.IsRepeat)
+ {
+ tags.Add("Repeat");
+ }
+
+ if (info.IsMovie)
+ {
+ tags.Add("Movie");
+ }
+
+ if (isSeries)
+ {
+ tags.Add("Series");
+ }
+
+ item.Tags = tags.ToArray();
+
+ item.Genres = info.Genres.ToArray();
+
+ if (info.IsHD ?? false)
+ {
+ item.Width = 1280;
+ item.Height = 720;
+ }
+
+ item.IsMovie = info.IsMovie;
+ item.IsRepeat = info.IsRepeat;
+
+ if (item.IsSeries != isSeries)
+ {
+ forceUpdate = true;
+ }
+
+ item.IsSeries = isSeries;
+
+ item.Name = info.Name;
+ item.OfficialRating ??= info.OfficialRating;
+ item.Overview ??= info.Overview;
+ item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
+ item.ProviderIds = info.ProviderIds;
+
+ foreach (var providerId in info.SeriesProviderIds)
+ {
+ info.ProviderIds["Series" + providerId.Key] = providerId.Value;
+ }
+
+ if (item.StartDate != info.StartDate)
+ {
+ forceUpdate = true;
+ }
+
+ item.StartDate = info.StartDate;
+
+ if (item.EndDate != info.EndDate)
+ {
+ forceUpdate = true;
+ }
+
+ item.EndDate = info.EndDate;
+
+ item.ProductionYear = info.ProductionYear;
+
+ if (!isSeries || info.IsRepeat)
+ {
+ item.PremiereDate = info.OriginalAirDate;
+ }
+
+ item.IndexNumber = info.EpisodeNumber;
+ item.ParentIndexNumber = info.SeasonNumber;
+
+ if (!item.HasImage(ImageType.Primary))
+ {
+ if (!string.IsNullOrWhiteSpace(info.ImagePath))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ImagePath,
+ Type = ImageType.Primary
+ },
+ 0);
+ }
+ else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ImageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
+ }
+ }
+
+ if (!item.HasImage(ImageType.Thumb))
+ {
+ if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ThumbImageUrl,
+ Type = ImageType.Thumb
+ },
+ 0);
+ }
+ }
+
+ if (!item.HasImage(ImageType.Logo))
+ {
+ if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.LogoImageUrl,
+ Type = ImageType.Logo
+ },
+ 0);
+ }
+ }
+
+ if (!item.HasImage(ImageType.Backdrop))
+ {
+ if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.BackdropImageUrl,
+ Type = ImageType.Backdrop
+ },
+ 0);
+ }
+ }
+
+ var isUpdated = false;
+ if (isNew)
+ {
+ }
+ else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
+ {
+ isUpdated = true;
+ }
+ else
+ {
+ var etag = info.Etag;
+
+ if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
+ {
+ item.SetProviderId(EtagKey, etag);
+ isUpdated = true;
+ }
+ }
+
+ if (isNew || isUpdated)
+ {
+ item.OnMetadataChanged();
+ }
+
+ return (item, isNew, isUpdated);
+ }
+}
diff --git a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
new file mode 100644
index 0000000000..a9fde08501
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.LiveTv.Configuration;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Tasks;
+
+namespace Jellyfin.LiveTv.Guide;
+
+/// <summary>
+/// The "Refresh Guide" scheduled task.
+/// </summary>
+public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
+{
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly IGuideManager _guideManager;
+ private readonly IConfigurationManager _config;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
+ /// </summary>
+ /// <param name="liveTvManager">The live tv manager.</param>
+ /// <param name="guideManager">The guide manager.</param>
+ /// <param name="config">The configuration manager.</param>
+ public RefreshGuideScheduledTask(
+ ILiveTvManager liveTvManager,
+ IGuideManager guideManager,
+ IConfigurationManager config)
+ {
+ _liveTvManager = liveTvManager;
+ _guideManager = guideManager;
+ _config = config;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Refresh Guide";
+
+ /// <inheritdoc />
+ public string Description => "Downloads channel information from live tv services.";
+
+ /// <inheritdoc />
+ public string Category => "Live TV";
+
+ /// <inheritdoc />
+ public bool IsHidden => _liveTvManager.Services.Count == 1 && _config.GetLiveTvConfiguration().TunerHosts.Length == 0;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
+ /// <inheritdoc />
+ public string Key => "RefreshGuide";
+
+ /// <inheritdoc />
+ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ => _guideManager.RefreshGuide(progress, cancellationToken);
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[]
+ {
+ new TaskTriggerInfo
+ {
+ Type = TaskTriggerInfo.TriggerInterval,
+ IntervalTicks = TimeSpan.FromHours(24).Ticks
+ }
+ };
+ }
+}
diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
new file mode 100644
index 0000000000..c58889740a
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
+ <_Parameter1>Jellyfin.LiveTv.Tests</_Parameter1>
+ </AssemblyAttribute>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AsyncKeyedLock" />
+ <PackageReference Include="Jellyfin.XmlTv" />
+ <PackageReference Include="System.Linq.Async" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
+ <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
index 5be3a7488a..c7a57859e8 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
@@ -16,9 +16,10 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
+using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.LiveTv;
@@ -27,7 +28,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.Listings
+namespace Jellyfin.LiveTv.Listings
{
public class SchedulesDirect : IListingsProvider, IDisposable
{
@@ -35,7 +36,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ILogger<SchedulesDirect> _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
+ private readonly AsyncNonKeyedLocker _tokenLock = new(1);
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -105,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token);
- using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
return Array.Empty<ProgramInfo>();
@@ -120,8 +120,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
- using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
- var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
+ .ConfigureAwait(false);
if (programDetails is null)
{
return Array.Empty<ProgramInfo>();
@@ -471,16 +471,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
str.Length--;
str.Append(']');
- using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
- {
- Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
- };
+ using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs");
message.Headers.TryAddWithoutValidation("token", token);
+ message.Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json);
try
{
- using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
- return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await Request<IReadOnlyList<ShowImagesDto>>(message, true, info, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -506,8 +503,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
- using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
- var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await Request<IReadOnlyList<HeadendsDto>>(options, false, info, cancellationToken).ConfigureAwait(false);
if (root is not null)
{
foreach (HeadendsDto headend in root)
@@ -573,49 +569,50 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
- savedToken.Name = result;
- savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
- return result;
- }
- catch (HttpRequestException ex)
+ using (await _tokenLock.LockAsync(cancellationToken).ConfigureAwait(false))
{
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ try
{
- _tokens.Clear();
- _lastErrorResponse = DateTime.UtcNow;
+ var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false);
+ savedToken.Name = result;
+ savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture);
+ return result;
}
+ catch (HttpRequestException ex)
+ {
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ {
+ _tokens.Clear();
+ _lastErrorResponse = DateTime.UtcNow;
+ }
- throw;
- }
- finally
- {
- _tokenSemaphore.Release();
+ throw;
+ }
}
}
- private async Task<HttpResponseMessage> Send(
- HttpRequestMessage options,
+ private async Task<T> Request<T>(
+ HttpRequestMessage message,
bool enableRetry,
ListingsProviderInfo providerInfo,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
- var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(message, completionOption, cancellationToken)
+ .ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
- return response;
+ return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken).ConfigureAwait(false);
}
- // Response is automatically disposed in the calling function,
- // so dispose manually if not returning.
- response.Dispose();
if (!enableRetry || (int)response.StatusCode >= 500)
{
+ _logger.LogError(
+ "Request to {Url} failed with response {Response}",
+ message.RequestUri,
+ await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+
throw new HttpRequestException(
string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
null,
@@ -623,8 +620,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
_tokens.Clear();
- options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
- return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
+ using var retryMessage = new HttpRequestMessage(message.Method, message.RequestUri);
+ retryMessage.Content = message.Content;
+ retryMessage.Headers.TryAddWithoutValidation(
+ "token",
+ await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
+
+ return await Request<T>(retryMessage, false, providerInfo, cancellationToken).ConfigureAwait(false);
}
private async Task<string> GetTokenInternal(
@@ -641,9 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
- using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await Request<TokenDto>(options, false, null, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@@ -660,11 +660,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings
ArgumentException.ThrowIfNullOrEmpty(token);
ArgumentException.ThrowIfNullOrEmpty(info.ListingsId);
- _logger.LogInformation("Adding new LineUp ");
+ _logger.LogInformation("Adding new lineup {Id}", info.ListingsId);
- using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId);
- options.Headers.TryAddWithoutValidation("token", token);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ using var message = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId);
+ message.Headers.TryAddWithoutValidation("token", token);
+
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogError(
+ "Error adding lineup to account: {Response}",
+ await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+ }
}
private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -682,9 +692,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
- using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
- httpResponse.EnsureSuccessStatusCode();
- var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await Request<LineupsDto>(options, false, null, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
@@ -737,8 +745,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
options.Headers.TryAddWithoutValidation("token", token);
- using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await Request<ChannelDto>(options, true, info, cancellationToken).ConfigureAwait(false);
if (root is null)
{
return new List<ChannelInfo>();
@@ -799,7 +806,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (disposing)
{
- _tokenSemaphore?.Dispose();
+ _tokenLock?.Dispose();
}
_disposed = true;
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
index 95ac996e01..c1a502fd5a 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Broadcaster dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
index f6251b9ad8..0cc39f3bb5 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Caption dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
index 0b7a2c63ad..bdcf87fdac 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Cast dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
index 87c327ed82..4e0d740785 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Channel dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
index c19cd2e48d..5c624c2885 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Content rating dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
index f00c9accdd..6d3c79c185 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Crew dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
index 1a371965cf..094f9a3194 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Day dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
index ca6ae7fb13..0063f4cc34 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Description 1_000 dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
index 1577219ed2..1d9a18cc7f 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Description 100 dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
index eaf4a340bd..75e91547bb 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Descriptions program dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
index fbdfb1f716..28abe094ed 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Event details dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
index 6852d89d71..6eefc1744b 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Gracenote dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
index b9844562f3..a62ae61f91 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Headends dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
index a1ae3ca6d4..21b595f244 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Image data dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
index 3dc64e5d8a..856b7a89b0 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// The lineup dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
index f190817813..99f80ce8a1 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Lineups dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
index fecc55e037..d7836384e2 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Logo dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
index ffd02d474b..ea583a1cea 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Map dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
index 40faa493c5..cafc8e2738 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Metadata dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
index 43f2901566..243ccff5c0 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Metadata programs dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
index 04560ab55d..1c5c5333ce 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
@@ -1,7 +1,7 @@
using System;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Metadata schedule dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
index 31bef423b2..aea7408336 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Movie dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
index e8b15dc07c..328cefadc3 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Multipart dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
index 84c48f67f3..8c3906f863 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Program details dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
index 60389b45bf..527a6f8a19 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Program dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
index c5ddcf7c51..61496155ac 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Quality rating dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
index e04b619a4d..287cd4ed5c 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Rating dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
index c8f79fd1c2..d380ec7aee 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Recommendation dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
index 0cd05709b3..6fc695a393 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Request schedule for channel dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
index 84e224b71e..523900a96a 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Show image dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
index d797fd49b1..dbde1e117d 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Station dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
index 61cd4a9b00..146124f987 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// Title dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
index afb9994869..b3bc618376 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
@@ -1,7 +1,7 @@
using System;
using System.Text.Json.Serialization;
-namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
{
/// <summary>
/// The token dto.
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
index e60e9dcc1c..cecc363f07 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
@@ -23,7 +23,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.Listings
+namespace Jellyfin.LiveTv.Listings
{
public class XmlTvListingsProvider : IListingsProvider
{
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/src/Jellyfin.LiveTv/LiveTvDtoService.cs
index 9326fbd5c7..55b056d3d8 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/src/Jellyfin.LiveTv/LiveTvDtoService.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
@@ -20,7 +21,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv
+namespace Jellyfin.LiveTv
{
public class LiveTvDtoService
{
@@ -456,7 +457,7 @@ namespace Emby.Server.Implementations.LiveTv
info.Id = timer.ExternalId;
}
- if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId))
+ if (!dto.ChannelId.IsEmpty() && string.IsNullOrEmpty(info.ChannelId))
{
var channel = _libraryManager.GetItemById(dto.ChannelId);
@@ -522,7 +523,7 @@ namespace Emby.Server.Implementations.LiveTv
info.Id = timer.ExternalId;
}
- if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId))
+ if (!dto.ChannelId.IsEmpty() && string.IsNullOrEmpty(info.ChannelId))
{
var channel = _libraryManager.GetItemById(dto.ChannelId);
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs
index 426165de61..ef5283b980 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/src/Jellyfin.LiveTv/LiveTvManager.cs
@@ -9,86 +9,77 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.Library;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Configuration;
+using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.Guide;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv
+namespace Jellyfin.LiveTv
{
/// <summary>
/// Class LiveTvManager.
/// </summary>
public class LiveTvManager : ILiveTvManager
{
- private const int MaxGuideDays = 14;
- private const string ExternalServiceTag = "ExternalServiceId";
-
- private const string EtagKey = "ProgramEtag";
-
private readonly IServerConfigurationManager _config;
private readonly ILogger<LiveTvManager> _logger;
- private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization;
- private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
-
- private ILiveTvService[] _services = Array.Empty<ILiveTvService>();
- private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
- private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
+ private readonly ILiveTvService[] _services;
+ private readonly IListingsProvider[] _listingProviders;
public LiveTvManager(
IServerConfigurationManager config,
ILogger<LiveTvManager> logger,
- IItemRepository itemRepo,
IUserDataManager userDataManager,
IDtoService dtoService,
IUserManager userManager,
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
- IFileSystem fileSystem,
IChannelManager channelManager,
- LiveTvDtoService liveTvDtoService)
+ LiveTvDtoService liveTvDtoService,
+ IEnumerable<ILiveTvService> services,
+ IEnumerable<IListingsProvider> listingProviders)
{
_config = config;
_logger = logger;
- _itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
- _fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
_channelManager = channelManager;
_tvDtoService = liveTvDtoService;
+ _services = services.ToArray();
+ _listingProviders = listingProviders.ToArray();
+
+ var defaultService = _services.OfType<EmbyTV.EmbyTV>().First();
+ defaultService.TimerCreated += OnEmbyTvTimerCreated;
+ defaultService.TimerCancelled += OnEmbyTvTimerCancelled;
}
public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
@@ -105,43 +96,13 @@ namespace Emby.Server.Implementations.LiveTv
/// <value>The services.</value>
public IReadOnlyList<ILiveTvService> Services => _services;
- public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
-
public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
- private LiveTvOptions GetConfiguration()
- {
- return _config.GetConfiguration<LiveTvOptions>("livetv");
- }
-
public string GetEmbyTvActiveRecordingPath(string id)
{
return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
}
- /// <summary>
- /// Adds the parts.
- /// </summary>
- /// <param name="services">The services.</param>
- /// <param name="tunerHosts">The tuner hosts.</param>
- /// <param name="listingProviders">The listing providers.</param>
- public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders)
- {
- _services = services.ToArray();
- _tunerHosts = tunerHosts.Where(i => i.IsSupported).ToArray();
-
- _listingProviders = listingProviders.ToArray();
-
- foreach (var service in _services)
- {
- if (service is EmbyTV.EmbyTV embyTv)
- {
- embyTv.TimerCreated += OnEmbyTvTimerCreated;
- embyTv.TimerCancelled += OnEmbyTvTimerCancelled;
- }
- }
- }
-
private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs<string> e)
{
var timerId = e.Argument;
@@ -160,20 +121,6 @@ namespace Emby.Server.Implementations.LiveTv
}));
}
- public List<NameIdPair> GetTunerHostTypes()
- {
- return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = i.Type
- }).ToList();
- }
-
- public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
- {
- return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken);
- }
-
public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
{
var user = query.UserId.Equals(default)
@@ -231,7 +178,9 @@ namespace Emby.Server.Implementations.LiveTv
_logger.LogInformation("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
MediaSourceInfo info;
+#pragma warning disable CA1859 // TODO: Analyzer bug?
ILiveStream liveStream;
+#pragma warning restore CA1859
if (service is ISupportsDirectStreamProvider supportsManagedStream)
{
liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
@@ -424,355 +373,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private async Task<LiveTvChannel> GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
- {
- var parentFolderId = parentFolder.Id;
- var isNew = false;
- var forceUpdate = false;
-
- var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id);
-
- var item = _libraryManager.GetItemById(id) as LiveTvChannel;
-
- if (item is null)
- {
- item = new LiveTvChannel
- {
- Name = channelInfo.Name,
- Id = id,
- DateCreated = DateTime.UtcNow
- };
-
- isNew = true;
- }
-
- if (channelInfo.Tags is not null)
- {
- if (!channelInfo.Tags.SequenceEqual(item.Tags, StringComparer.OrdinalIgnoreCase))
- {
- isNew = true;
- }
-
- item.Tags = channelInfo.Tags;
- }
-
- if (!item.ParentId.Equals(parentFolderId))
- {
- isNew = true;
- }
-
- item.ParentId = parentFolderId;
-
- item.ChannelType = channelInfo.ChannelType;
- item.ServiceName = serviceName;
-
- if (!string.Equals(item.GetProviderId(ExternalServiceTag), serviceName, StringComparison.OrdinalIgnoreCase))
- {
- forceUpdate = true;
- }
-
- item.SetProviderId(ExternalServiceTag, serviceName);
-
- if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
- {
- forceUpdate = true;
- }
-
- item.ExternalId = channelInfo.Id;
-
- if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
- {
- forceUpdate = true;
- }
-
- item.Number = channelInfo.Number;
-
- if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
- {
- forceUpdate = true;
- }
-
- item.Name = channelInfo.Name;
-
- if (!item.HasImage(ImageType.Primary))
- {
- if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
- {
- item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
- forceUpdate = true;
- }
- else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl))
- {
- item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
- forceUpdate = true;
- }
- }
-
- if (isNew)
- {
- _libraryManager.CreateItem(item, parentFolder);
- }
- else if (forceUpdate)
- {
- await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
- }
-
- return item;
- }
-
- private (LiveTvProgram Item, bool IsNew, bool IsUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel)
- {
- var id = _tvDtoService.GetInternalProgramId(info.Id);
-
- var isNew = false;
- var forceUpdate = false;
-
- if (!allExistingPrograms.TryGetValue(id, out LiveTvProgram item))
- {
- isNew = true;
- item = new LiveTvProgram
- {
- Name = info.Name,
- Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow
- };
-
- if (!string.IsNullOrEmpty(info.Etag))
- {
- item.SetProviderId(EtagKey, info.Etag);
- }
- }
-
- if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
- {
- item.ShowId = info.ShowId;
- forceUpdate = true;
- }
-
- var seriesId = info.SeriesId;
-
- if (!item.ParentId.Equals(channel.Id))
- {
- forceUpdate = true;
- }
-
- item.ParentId = channel.Id;
-
- item.Audio = info.Audio;
- item.ChannelId = channel.Id;
- item.CommunityRating ??= info.CommunityRating;
- if ((item.CommunityRating ?? 0).Equals(0))
- {
- item.CommunityRating = null;
- }
-
- item.EpisodeTitle = info.EpisodeTitle;
- item.ExternalId = info.Id;
-
- if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
- {
- forceUpdate = true;
- }
-
- item.ExternalSeriesId = seriesId;
-
- var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
-
- if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
- {
- item.SeriesName = info.Name;
- }
-
- var tags = new List<string>();
- if (info.IsLive)
- {
- tags.Add("Live");
- }
-
- if (info.IsPremiere)
- {
- tags.Add("Premiere");
- }
-
- if (info.IsNews)
- {
- tags.Add("News");
- }
-
- if (info.IsSports)
- {
- tags.Add("Sports");
- }
-
- if (info.IsKids)
- {
- tags.Add("Kids");
- }
-
- if (info.IsRepeat)
- {
- tags.Add("Repeat");
- }
-
- if (info.IsMovie)
- {
- tags.Add("Movie");
- }
-
- if (isSeries)
- {
- tags.Add("Series");
- }
-
- item.Tags = tags.ToArray();
-
- item.Genres = info.Genres.ToArray();
-
- if (info.IsHD ?? false)
- {
- item.Width = 1280;
- item.Height = 720;
- }
-
- item.IsMovie = info.IsMovie;
- item.IsRepeat = info.IsRepeat;
-
- if (item.IsSeries != isSeries)
- {
- forceUpdate = true;
- }
-
- item.IsSeries = isSeries;
-
- item.Name = info.Name;
- item.OfficialRating ??= info.OfficialRating;
- item.Overview ??= info.Overview;
- item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
- item.ProviderIds = info.ProviderIds;
-
- foreach (var providerId in info.SeriesProviderIds)
- {
- info.ProviderIds["Series" + providerId.Key] = providerId.Value;
- }
-
- if (item.StartDate != info.StartDate)
- {
- forceUpdate = true;
- }
-
- item.StartDate = info.StartDate;
-
- if (item.EndDate != info.EndDate)
- {
- forceUpdate = true;
- }
-
- item.EndDate = info.EndDate;
-
- item.ProductionYear = info.ProductionYear;
-
- if (!isSeries || info.IsRepeat)
- {
- item.PremiereDate = info.OriginalAirDate;
- }
-
- item.IndexNumber = info.EpisodeNumber;
- item.ParentIndexNumber = info.SeasonNumber;
-
- if (!item.HasImage(ImageType.Primary))
- {
- if (!string.IsNullOrWhiteSpace(info.ImagePath))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ImagePath,
- Type = ImageType.Primary
- },
- 0);
- }
- else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ImageUrl,
- Type = ImageType.Primary
- },
- 0);
- }
- }
-
- if (!item.HasImage(ImageType.Thumb))
- {
- if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ThumbImageUrl,
- Type = ImageType.Thumb
- },
- 0);
- }
- }
-
- if (!item.HasImage(ImageType.Logo))
- {
- if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.LogoImageUrl,
- Type = ImageType.Logo
- },
- 0);
- }
- }
-
- if (!item.HasImage(ImageType.Backdrop))
- {
- if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.BackdropImageUrl,
- Type = ImageType.Backdrop
- },
- 0);
- }
- }
-
- var isUpdated = false;
- if (isNew)
- {
- }
- else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
- {
- isUpdated = true;
- }
- else
- {
- var etag = info.Etag;
-
- if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
- {
- item.SetProviderId(EtagKey, etag);
- isUpdated = true;
- }
- }
-
- if (isNew || isUpdated)
- {
- item.OnMetadataChanged();
- }
-
- return (item, isNew, isUpdated);
- }
-
public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
{
var program = _libraryManager.GetItemById(id);
@@ -1024,293 +624,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- internal Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken)
- {
- return RefreshChannelsInternal(progress, cancellationToken);
- }
-
- private async Task RefreshChannelsInternal(IProgress<double> progress, CancellationToken cancellationToken)
- {
- await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false);
-
- await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
-
- var numComplete = 0;
- double progressPerService = _services.Length == 0
- ? 0
- : 1.0 / _services.Length;
-
- var newChannelIdList = new List<Guid>();
- var newProgramIdList = new List<Guid>();
-
- var cleanDatabase = true;
-
- foreach (var service in _services)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- _logger.LogDebug("Refreshing guide from {Name}", service.Name);
-
- try
- {
- var innerProgress = new ActionableProgress<double>();
- innerProgress.RegisterAction(p => progress.Report(p * progressPerService));
-
- var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false);
-
- newChannelIdList.AddRange(idList.Item1);
- newProgramIdList.AddRange(idList.Item2);
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
- {
- cleanDatabase = false;
- _logger.LogError(ex, "Error refreshing channels for service");
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= _services.Length;
-
- progress.Report(100 * percent);
- }
-
- if (cleanDatabase)
- {
- CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken);
- CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken);
- }
-
- var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
-
- if (coreService is not null)
- {
- await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
- await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
- }
-
- // Load these now which will prefetch metadata
- var dtoOptions = new DtoOptions();
- var fields = dtoOptions.Fields.ToList();
- dtoOptions.Fields = fields.ToArray();
-
- progress.Report(100);
- }
-
- private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, ActionableProgress<double> progress, CancellationToken cancellationToken)
- {
- progress.Report(10);
-
- var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
- .Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
- .ToList();
-
- var list = new List<LiveTvChannel>();
-
- var numComplete = 0;
- var parentFolder = GetInternalLiveTvFolder(cancellationToken);
-
- foreach (var channelInfo in allChannelsList)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- try
- {
- var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false);
-
- list.Add(item);
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
- }
-
- numComplete++;
- double percent = numComplete;
- percent /= allChannelsList.Count;
-
- progress.Report((5 * percent) + 10);
- }
-
- progress.Report(15);
-
- numComplete = 0;
- var programs = new List<Guid>();
- var channels = new List<Guid>();
-
- var guideDays = GetGuideDays();
-
- _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- foreach (var currentChannel in list)
- {
- channels.Add(currentChannel.Id);
- cancellationToken.ThrowIfCancellationRequested();
-
- try
- {
- var start = DateTime.UtcNow.AddHours(-1);
- var end = start.AddDays(guideDays);
-
- var isMovie = false;
- var isSports = false;
- var isNews = false;
- var isKids = false;
- var iSSeries = false;
-
- var channelPrograms = (await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false)).ToList();
-
- var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
- ChannelIds = new Guid[] { currentChannel.Id },
- DtoOptions = new DtoOptions(true)
- }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
-
- var newPrograms = new List<LiveTvProgram>();
- var updatedPrograms = new List<BaseItem>();
-
- foreach (var program in channelPrograms)
- {
- var programTuple = GetProgram(program, existingPrograms, currentChannel);
- var programItem = programTuple.Item;
-
- if (programTuple.IsNew)
- {
- newPrograms.Add(programItem);
- }
- else if (programTuple.IsUpdated)
- {
- updatedPrograms.Add(programItem);
- }
-
- programs.Add(programItem.Id);
-
- isMovie |= program.IsMovie;
- iSSeries |= program.IsSeries;
- isSports |= program.IsSports;
- isNews |= program.IsNews;
- isKids |= program.IsKids;
- }
-
- _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
-
- if (newPrograms.Count > 0)
- {
- _libraryManager.CreateItems(newPrograms, null, cancellationToken);
- }
-
- if (updatedPrograms.Count > 0)
- {
- await _libraryManager.UpdateItemsAsync(
- updatedPrograms,
- currentChannel,
- ItemUpdateType.MetadataImport,
- cancellationToken).ConfigureAwait(false);
- }
-
- currentChannel.IsMovie = isMovie;
- currentChannel.IsNews = isNews;
- currentChannel.IsSports = isSports;
- currentChannel.IsSeries = iSSeries;
-
- if (isKids)
- {
- currentChannel.AddTag("Kids");
- }
-
- await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
- await currentChannel.RefreshMetadata(
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- },
- cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException)
- {
- throw;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
- }
-
- numComplete++;
- double percent = numComplete / (double)allChannelsList.Count;
-
- progress.Report((85 * percent) + 15);
- }
-
- progress.Report(100);
- return new Tuple<List<Guid>, List<Guid>>(channels, programs);
- }
-
- private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
- {
- var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
- {
- IncludeItemTypes = validTypes,
- DtoOptions = new DtoOptions(false)
- });
-
- var numComplete = 0;
-
- foreach (var itemId in list)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (itemId.Equals(default))
- {
- // Somehow some invalid data got into the db. It probably predates the boundary checking
- continue;
- }
-
- if (!currentIdList.Contains(itemId))
- {
- var item = _libraryManager.GetItemById(itemId);
-
- if (item is not null)
- {
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false,
- DeleteFromExternalProvider = false
- },
- false);
- }
- }
-
- numComplete++;
- double percent = numComplete / (double)list.Count;
-
- progress.Report(100 * percent);
- }
- }
-
- private double GetGuideDays()
- {
- var config = GetConfiguration();
-
- if (config.GuideDays.HasValue)
- {
- return Math.Max(1, Math.Min(config.GuideDays.Value, MaxGuideDays));
- }
-
- return 7;
- }
-
private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
{
if (user is null)
@@ -1842,12 +1155,6 @@ namespace Emby.Server.Implementations.LiveTv
return new QueryResult<SeriesTimerInfoDto>(returnArray);
}
- public BaseItem GetLiveTvChannel(TimerInfo timer, ILiveTvService service)
- {
- var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, timer.ChannelId);
- return _libraryManager.GetItemById(internalChannelId);
- }
-
public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user)
{
var now = DateTime.UtcNow;
@@ -2080,18 +1387,6 @@ namespace Emby.Server.Implementations.LiveTv
await service.UpdateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
}
- public GuideInfo GetGuideInfo()
- {
- var startDate = DateTime.UtcNow;
- var endDate = startDate.AddDays(GetGuideDays());
-
- return new GuideInfo
- {
- StartDate = startDate,
- EndDate = endDate
- };
- }
-
private LiveTvServiceInfo[] GetServiceInfos()
{
return Services.Select(GetServiceInfo).ToArray();
@@ -2124,7 +1419,7 @@ namespace Emby.Server.Implementations.LiveTv
private bool IsLiveTvEnabled(User user)
{
- return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
+ return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || _config.GetLiveTvConfiguration().TunerHosts.Length > 0);
}
public IEnumerable<User> GetEnabledUsers()
@@ -2170,48 +1465,6 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
}
- public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
- {
- info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
-
- var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
-
- if (provider is null)
- {
- throw new ResourceNotFoundException();
- }
-
- if (provider is IConfigurableTunerHost configurable)
- {
- await configurable.Validate(info).ConfigureAwait(false);
- }
-
- var config = GetConfiguration();
-
- var list = config.TunerHosts.ToList();
- var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
-
- if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
- {
- info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- list.Add(info);
- config.TunerHosts = list.ToArray();
- }
- else
- {
- config.TunerHosts[index] = info;
- }
-
- _config.SaveConfiguration("livetv", config);
-
- if (dataSourceChanged)
- {
- _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
- }
-
- return info;
- }
-
public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
{
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
@@ -2231,7 +1484,7 @@ namespace Emby.Server.Implementations.LiveTv
await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);
- LiveTvOptions config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
var list = config.ListingProviders.ToList();
int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
@@ -2256,7 +1509,7 @@ namespace Emby.Server.Implementations.LiveTv
public void DeleteListingsProvider(string id)
{
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
@@ -2266,7 +1519,7 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
@@ -2326,7 +1579,7 @@ namespace Emby.Server.Implementations.LiveTv
public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location)
{
- var config = GetConfiguration();
+ var config = _config.GetLiveTvConfiguration();
if (string.IsNullOrWhiteSpace(providerId))
{
@@ -2356,27 +1609,17 @@ namespace Emby.Server.Implementations.LiveTv
public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken)
{
- var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
+ var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken);
}
public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken)
{
- var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
+ var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase));
return provider.GetChannels(info, cancellationToken);
}
- public Guid GetInternalChannelId(string serviceName, string externalId)
- {
- return _tvDtoService.GetInternalChannelId(serviceName, externalId);
- }
-
- public Guid GetInternalProgramId(string externalId)
- {
- return _tvDtoService.GetInternalProgramId(externalId);
- }
-
/// <inheritdoc />
public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
=> GetRecordingFoldersAsync(user, false);
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs
index 6a92fc5997..ce9361089c 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs
@@ -16,7 +16,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv
+namespace Jellyfin.LiveTv
{
public class LiveTvMediaSourceProvider : IMediaSourceProvider
{
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv
{
if (activeRecordingInfo is not null)
{
- sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken)
+ sources = await _mediaSourceManager.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken)
.ConfigureAwait(false);
}
else
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/src/Jellyfin.LiveTv/RecordingNotifier.cs
index e0ca02d986..226d525e71 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/src/Jellyfin.LiveTv/RecordingNotifier.cs
@@ -1,7 +1,3 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
using System;
using System.Linq;
using System.Threading;
@@ -10,34 +6,44 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.EntryPoints
+namespace Jellyfin.LiveTv
{
- public sealed class RecordingNotifier : IServerEntryPoint
+ /// <summary>
+ /// <see cref="IHostedService"/> responsible for notifying users when a LiveTV recording is completed.
+ /// </summary>
+ public sealed class RecordingNotifier : IHostedService
{
- private readonly ILiveTvManager _liveTvManager;
+ private readonly ILogger<RecordingNotifier> _logger;
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
- private readonly ILogger<RecordingNotifier> _logger;
+ private readonly ILiveTvManager _liveTvManager;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RecordingNotifier"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
+ /// <param name="userManager">The <see cref="IUserManager"/>.</param>
+ /// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
public RecordingNotifier(
+ ILogger<RecordingNotifier> logger,
ISessionManager sessionManager,
IUserManager userManager,
- ILogger<RecordingNotifier> logger,
ILiveTvManager liveTvManager)
{
+ _logger = logger;
_sessionManager = sessionManager;
_userManager = userManager;
- _logger = logger;
_liveTvManager = liveTvManager;
}
/// <inheritdoc />
- public Task RunAsync()
+ public Task StartAsync(CancellationToken cancellationToken)
{
_liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled;
_liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled;
@@ -47,29 +53,35 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
- private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken)
{
- await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
- }
+ _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
+ _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
+ _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
+ _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
- private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
- {
- await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
+ return Task.CompletedTask;
}
- private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
- {
- await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
- }
+ private async void OnLiveTvManagerSeriesTimerCreated(object? sender, GenericEventArgs<TimerEventInfo> e)
+ => await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
- private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
- {
- await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
- }
+ private async void OnLiveTvManagerTimerCreated(object? sender, GenericEventArgs<TimerEventInfo> e)
+ => await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
+
+ private async void OnLiveTvManagerSeriesTimerCancelled(object? sender, GenericEventArgs<TimerEventInfo> e)
+ => await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
+
+ private async void OnLiveTvManagerTimerCancelled(object? sender, GenericEventArgs<TimerEventInfo> e)
+ => await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{
- var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
+ var users = _userManager.Users
+ .Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess))
+ .Select(i => i.Id)
+ .ToList();
try
{
@@ -80,14 +92,5 @@ namespace Emby.Server.Implementations.EntryPoints
_logger.LogError(ex, "Error sending message");
}
}
-
- /// <inheritdoc />
- public void Dispose()
- {
- _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled;
- _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled;
- _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated;
- _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated;
- }
}
}
diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/src/Jellyfin.LiveTv/StreamHelper.cs
index 6eaf22ce48..e9644e95e7 100644
--- a/Emby.Server.Implementations/IO/StreamHelper.cs
+++ b/src/Jellyfin.LiveTv/StreamHelper.cs
@@ -7,7 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
-namespace Emby.Server.Implementations.IO
+namespace Jellyfin.LiveTv
{
public class StreamHelper : IStreamHelper
{
@@ -81,36 +81,6 @@ namespace Emby.Server.Implementations.IO
}
}
- public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
- {
- byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
- try
- {
- int bytesRead;
-
- while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
- {
- var bytesToWrite = Math.Min(bytesRead, copyLength);
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(buffer.AsMemory(0, Convert.ToInt32(bytesToWrite)), cancellationToken).ConfigureAwait(false);
- }
-
- copyLength -= bytesToWrite;
-
- if (copyLength <= 0)
- {
- break;
- }
- }
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buffer);
- }
- }
-
public async Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs
index da597056a4..afc2e4f9ce 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs
@@ -10,7 +10,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
+using Jellyfin.LiveTv.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -19,7 +19,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts
+namespace Jellyfin.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
@@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
protected virtual IList<TunerHostInfo> GetTunerHosts()
{
- return GetConfiguration().TunerHosts
+ return Config.GetLiveTvConfiguration().TunerHosts
.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
.ToList();
}
@@ -228,10 +228,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
}
-
- protected LiveTvOptions GetConfiguration()
- {
- return Config.GetConfiguration<LiveTvOptions>("livetv");
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/Channels.cs
index 0f04531896..311a71d137 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/Channels.cs
@@ -1,6 +1,6 @@
#nullable disable
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
internal class Channels
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
index 39b3571422..3ece181f25 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs
@@ -2,7 +2,7 @@
using System;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
internal class DiscoverResponse
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
index aae33503fa..50a887826a 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 79e15a82e2..fef84dd000 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -30,7 +29,7 @@ using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
@@ -143,7 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
{
const string DefaultValue = "HDHR";
- var response = new DiscoverResponse
+ var discoverResponse = new DiscoverResponse
{
ModelNumber = DefaultValue
};
@@ -152,163 +151,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// HDHR4 doesn't have this api
lock (_modelCache)
{
- _modelCache[cacheKey] = response;
+ _modelCache[cacheKey] = discoverResponse;
}
}
- return response;
+ return discoverResponse;
}
throw;
}
}
- private async Task<List<LiveTvTunerInfo>> GetTunerInfosHttp(TunerHostInfo info, CancellationToken cancellationToken)
- {
- var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
-
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
- .ConfigureAwait(false);
- var tuners = new List<LiveTvTunerInfo>();
- var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (stream.ConfigureAwait(false))
- {
- using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
- await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
- {
- string stripedLine = StripXML(line);
- if (stripedLine.Contains("Channel", StringComparison.Ordinal))
- {
- LiveTvTunerStatus status;
- var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = stripedLine.Substring(0, index - 1);
- var currentChannel = stripedLine.Substring(index + 7);
- if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
- {
- status = LiveTvTunerStatus.LiveTv;
- }
- else
- {
- status = LiveTvTunerStatus.Available;
- }
-
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
- }
- }
- }
-
- return tuners;
- }
-
- private static string StripXML(string source)
- {
- if (string.IsNullOrEmpty(source))
- {
- return string.Empty;
- }
-
- char[] buffer = new char[source.Length];
- int bufferIndex = 0;
- bool inside = false;
-
- for (int i = 0; i < source.Length; i++)
- {
- char let = source[i];
- if (let == '<')
- {
- inside = true;
- continue;
- }
-
- if (let == '>')
- {
- inside = false;
- continue;
- }
-
- if (!inside)
- {
- buffer[bufferIndex++] = let;
- }
- }
-
- return new string(buffer, 0, bufferIndex);
- }
-
- private async Task<List<LiveTvTunerInfo>> GetTunerInfosUdp(TunerHostInfo info, CancellationToken cancellationToken)
- {
- var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
-
- var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
-
- var uri = new Uri(GetApiUrl(info));
-
- using (var manager = new HdHomerunManager())
- {
- // Legacy HdHomeruns are IPv4 only
- var ipInfo = IPAddress.Parse(uri.Host);
-
- for (int i = 0; i < model.TunerCount; i++)
- {
- var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
- var currentChannel = "none"; // TODO: Get current channel and map back to Station Id
- var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
- var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
- }
- }
-
- return tuners;
- }
-
- public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
- {
- var list = new List<LiveTvTunerInfo>();
-
- foreach (var host in GetConfiguration().TunerHosts
- .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
- {
- try
- {
- list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false));
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting tuner info");
- }
- }
-
- return list;
- }
-
- public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
- {
- // TODO Need faster way to determine UDP vs HTTP
- var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false);
-
- var hdHomerunChannelInfo = channels.FirstOrDefault() as HdHomerunChannelInfo;
-
- if (hdHomerunChannelInfo is null || hdHomerunChannelInfo.IsLegacyTuner)
- {
- return await GetTunerInfosUdp(info, cancellationToken).ConfigureAwait(false);
- }
-
- return await GetTunerInfosHttp(info, cancellationToken).ConfigureAwait(false);
- }
-
private static string GetApiUrl(TunerHostInfo info)
{
var url = info.Url;
@@ -574,40 +427,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_streamHelper);
}
- var enableHttpStream = true;
- if (enableHttpStream)
- {
- mediaSource.Protocol = MediaProtocol.Http;
-
- var httpUrl = channel.Path;
-
- // If raw was used, the tuner doesn't support params
- if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
- {
- httpUrl += "?transcode=" + profile;
- }
+ mediaSource.Protocol = MediaProtocol.Http;
- mediaSource.Path = httpUrl;
+ var httpUrl = channel.Path;
- return new SharedHttpStream(
- mediaSource,
- tunerHost,
- streamId,
- FileSystem,
- _httpClientFactory,
- Logger,
- Config,
- _appHost,
- _streamHelper);
+ // If raw was used, the tuner doesn't support params
+ if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
+ {
+ httpUrl += "?transcode=" + profile;
}
- return new HdHomerunUdpStream(
+ mediaSource.Path = httpUrl;
+
+ return new SharedHttpStream(
mediaSource,
tunerHost,
streamId,
- new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
- modelInfo.TunerCount,
FileSystem,
+ _httpClientFactory,
Logger,
Config,
_appHost,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 68383a5547..8613387270 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -14,7 +14,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
public sealed class HdHomerunManager : IDisposable
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 6195c7648f..6c8cde62c9 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -1,5 +1,6 @@
#nullable disable
+#pragma warning disable CA1711
#pragma warning disable CS1591
using System;
@@ -19,7 +20,7 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
index 11bd40ab10..9fcf386f9c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
public interface IHdHomerunChannelCommands
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
index 654474e971..6dc9c885f6 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
@@ -3,7 +3,7 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
{
public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/src/Jellyfin.LiveTv/TunerHosts/LiveStream.cs
index c18594a29b..70d8afc5d0 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/LiveStream.cs
@@ -1,5 +1,6 @@
#nullable disable
+#pragma warning disable CA1711
#pragma warning disable CS1591
using System;
@@ -14,7 +15,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts
+namespace Jellyfin.LiveTv.TunerHosts
{
public class LiveStream : ILiveStream
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
index 11bf03b182..3666d342ed 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
@@ -24,7 +24,7 @@ using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts
+namespace Jellyfin.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
@@ -80,22 +80,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.ConfigureAwait(false);
}
- public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
- {
- var list = GetTunerHosts()
- .Select(i => new LiveTvTunerInfo()
- {
- Name = Name,
- SourceType = Type,
- Status = LiveTvTunerStatus.Available,
- Id = i.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture),
- Url = i.Url
- })
- .ToList();
-
- return Task.FromResult(list);
- }
-
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = tunerHost.TunerCount;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
index 0b5575b994..5900d1c5be 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
@@ -18,7 +18,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts
+namespace Jellyfin.LiveTv.TunerHosts
{
public partial class M3uParser
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/src/Jellyfin.LiveTv/TunerHosts/SharedHttpStream.cs
index efb84a5152..5ef04ad9e9 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/SharedHttpStream.cs
@@ -1,3 +1,4 @@
+#pragma warning disable CA1711
#pragma warning disable CS1591
using System;
@@ -16,7 +17,7 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-namespace Emby.Server.Implementations.LiveTv.TunerHosts
+namespace Jellyfin.LiveTv.TunerHosts
{
public class SharedHttpStream : LiveStream, IDirectStreamProvider
{
diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs
new file mode 100644
index 0000000000..60be19c68a
--- /dev/null
+++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.Guide;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.LiveTv.TunerHosts;
+
+/// <inheritdoc />
+public class TunerHostManager : ITunerHostManager
+{
+ private const int TunerDiscoveryDurationMs = 3000;
+
+ private readonly ILogger<TunerHostManager> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly ITaskManager _taskManager;
+ private readonly ITunerHost[] _tunerHosts;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TunerHostManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{T}"/>.</param>
+ /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+ /// <param name="taskManager">The <see cref="ITaskManager"/>.</param>
+ /// <param name="tunerHosts">The <see cref="IEnumerable{T}"/>.</param>
+ public TunerHostManager(
+ ILogger<TunerHostManager> logger,
+ IConfigurationManager config,
+ ITaskManager taskManager,
+ IEnumerable<ITunerHost> tunerHosts)
+ {
+ _logger = logger;
+ _config = config;
+ _taskManager = taskManager;
+ _tunerHosts = tunerHosts.Where(t => t.IsSupported).ToArray();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
+
+ /// <inheritdoc />
+ public IEnumerable<NameIdPair> GetTunerHostTypes()
+ => _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
+ {
+ Name = i.Name,
+ Id = i.Type
+ });
+
+ /// <inheritdoc />
+ public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
+ {
+ info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info))!;
+
+ var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
+
+ if (provider is null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ if (provider is IConfigurableTunerHost configurable)
+ {
+ await configurable.Validate(info).ConfigureAwait(false);
+ }
+
+ var config = _config.GetLiveTvConfiguration();
+
+ var list = config.TunerHosts.ToList();
+ var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
+
+ if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
+ {
+ info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ list.Add(info);
+ config.TunerHosts = list.ToArray();
+ }
+ else
+ {
+ config.TunerHosts[index] = info;
+ }
+
+ _config.SaveConfiguration("livetv", config);
+
+ if (dataSourceChanged)
+ {
+ _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
+ }
+
+ return info;
+ }
+
+ /// <inheritdoc />
+ public async IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly)
+ {
+ var configuredDeviceIds = _config.GetLiveTvConfiguration().TunerHosts
+ .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
+ .Select(i => i.DeviceId)
+ .ToList();
+
+ foreach (var host in _tunerHosts)
+ {
+ var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, CancellationToken.None).ConfigureAwait(false);
+ foreach (var tuner in discoveredDevices)
+ {
+ if (!newDevicesOnly || !configuredDeviceIds.Contains(tuner.DeviceId, StringComparer.OrdinalIgnoreCase))
+ {
+ yield return tuner;
+ }
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
+ {
+ foreach (var host in _tunerHosts)
+ {
+ await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
+ {
+ var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+ var configuredDevices = _config.GetLiveTvConfiguration().TunerHosts
+ .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ foreach (var device in discoveredDevices)
+ {
+ var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
+
+ if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url);
+
+ configuredDevice.Url = device.Url;
+ await SaveTunerHost(configuredDevice).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private async Task<IList<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false);
+
+ foreach (var device in discoveredDevices)
+ {
+ _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url);
+ }
+
+ return discoveredDevices;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error discovering tuner devices");
+
+ return Array.Empty<TunerHostInfo>();
+ }
+ }
+}
diff --git a/src/Jellyfin.Networking/ExternalPortForwarding.cs b/src/Jellyfin.Networking/PortForwardingHost.cs
index df9e43ca90..d01343624e 100644
--- a/src/Jellyfin.Networking/ExternalPortForwarding.cs
+++ b/src/Jellyfin.Networking/PortForwardingHost.cs
@@ -1,7 +1,3 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -12,36 +8,34 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Mono.Nat;
namespace Jellyfin.Networking;
/// <summary>
-/// Server entrypoint handling external port forwarding.
+/// <see cref="IHostedService"/> responsible for UPnP port forwarding.
/// </summary>
-public sealed class ExternalPortForwarding : IServerEntryPoint
+public sealed class PortForwardingHost : IHostedService, IDisposable
{
private readonly IServerApplicationHost _appHost;
- private readonly ILogger<ExternalPortForwarding> _logger;
+ private readonly ILogger<PortForwardingHost> _logger;
private readonly IServerConfigurationManager _config;
+ private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new();
- private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
-
- private Timer _timer;
- private string _configIdentifier;
-
+ private Timer? _timer;
+ private string? _configIdentifier;
private bool _disposed;
/// <summary>
- /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class.
+ /// Initializes a new instance of the <see cref="PortForwardingHost"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param>
- public ExternalPortForwarding(
- ILogger<ExternalPortForwarding> logger,
+ public PortForwardingHost(
+ ILogger<PortForwardingHost> logger,
IServerApplicationHost appHost,
IServerConfigurationManager config)
{
@@ -66,7 +60,7 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
.ToString();
}
- private void OnConfigurationUpdated(object sender, EventArgs e)
+ private void OnConfigurationUpdated(object? sender, EventArgs e)
{
var oldConfigIdentifier = _configIdentifier;
_configIdentifier = GetConfigIdentifier();
@@ -79,7 +73,7 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
}
/// <inheritdoc />
- public Task RunAsync()
+ public Task StartAsync(CancellationToken cancellationToken)
{
Start();
@@ -88,6 +82,14 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
return Task.CompletedTask;
}
+ /// <inheritdoc />
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ Stop();
+
+ return Task.CompletedTask;
+ }
+
private void Start()
{
var config = _config.GetNetworkConfiguration();
@@ -101,7 +103,8 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
NatUtility.DeviceFound += OnNatUtilityDeviceFound;
NatUtility.StartDiscovery();
- _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+ _timer?.Dispose();
+ _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
}
private void Stop()
@@ -112,13 +115,23 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
_timer?.Dispose();
+ _timer = null;
}
- private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
+ private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e)
{
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
try
{
- await CreateRules(e.Device).ConfigureAwait(false);
+ // On some systems the device discovered event seems to fire repeatedly
+ // This check will help ensure we're not trying to port map the same device over and over
+ if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0))
+ {
+ return;
+ }
+
+ await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -126,20 +139,6 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
}
}
- private Task CreateRules(INatDevice device)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
-
- // On some systems the device discovered event seems to fire repeatedly
- // This check will help ensure we're not trying to port map the same device over and over
- if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
- {
- return Task.CompletedTask;
- }
-
- return Task.WhenAll(CreatePortMaps(device));
- }
-
private IEnumerable<Task> CreatePortMaps(INatDevice device)
{
var config = _config.GetNetworkConfiguration();
@@ -185,8 +184,6 @@ public sealed class ExternalPortForwarding : IServerEntryPoint
_config.ConfigurationUpdated -= OnConfigurationUpdated;
- Stop();
-
_timer?.Dispose();
_timer = null;
diff --git a/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs
new file mode 100644
index 0000000000..dd84c1a186
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Controllers/SystemControllerTests.cs
@@ -0,0 +1,35 @@
+using Jellyfin.Api.Controllers;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.IO;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests.Controllers
+{
+ public class SystemControllerTests
+ {
+ [Fact]
+ public void GetLogFile_FileDoesNotExist_ReturnsNotFound()
+ {
+ var mockFileSystem = new Mock<IFileSystem>();
+ mockFileSystem
+ .Setup(fs => fs.GetFiles(It.IsAny<string>(), It.IsAny<bool>()))
+ .Returns([new() { Name = "file1.txt" }, new() { Name = "file2.txt" }]);
+
+ var controller = new SystemController(
+ Mock.Of<ILogger<SystemController>>(),
+ Mock.Of<IServerApplicationHost>(),
+ Mock.Of<IServerApplicationPaths>(),
+ mockFileSystem.Object,
+ Mock.Of<INetworkManager>(),
+ Mock.Of<ISystemManager>());
+
+ var result = controller.GetLogFile("DOES_NOT_EXIST.txt");
+
+ Assert.IsType<NotFoundObjectResult>(result);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.LiveTv.Tests/HdHomerunHostTests.cs
index 13ac3ddb0f..cb6eb91844 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
+++ b/tests/Jellyfin.LiveTv.Tests/HdHomerunHostTests.cs
@@ -6,13 +6,13 @@ using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun;
+using Jellyfin.LiveTv.TunerHosts.HdHomerun;
using MediaBrowser.Model.LiveTv;
using Moq;
using Moq.Protected;
using Xunit;
-namespace Jellyfin.Server.Implementations.Tests.LiveTv
+namespace Jellyfin.LiveTv.Tests
{
public class HdHomerunHostTests
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/HdHomerunManagerTests.cs
index fd499d9cf9..4ab0bd723c 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs
+++ b/tests/Jellyfin.LiveTv.Tests/HdHomerunManagerTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Text;
-using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun;
+using Jellyfin.LiveTv.TunerHosts.HdHomerun;
using Xunit;
-namespace Jellyfin.Server.Implementations.Tests.LiveTv
+namespace Jellyfin.LiveTv.Tests
{
public class HdHomerunManagerTests
{
diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
new file mode 100644
index 0000000000..f645f38c44
--- /dev/null
+++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" />
+ <PackageReference Include="AutoFixture.AutoMoq" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" />
+ <PackageReference Include="Moq" />
+ <PackageReference Include="xunit" />
+ <PackageReference Include="xunit.runner.visualstudio">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Xunit.SkippableFact" />
+ <PackageReference Include="coverlet.collector" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.LiveTv.Tests/Listings/XmlTvListingsProviderTests.cs
index 92b4178fdb..0fb7894e51 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
+++ b/tests/Jellyfin.LiveTv.Tests/Listings/XmlTvListingsProviderTests.cs
@@ -6,13 +6,13 @@ using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Emby.Server.Implementations.LiveTv.Listings;
+using Jellyfin.LiveTv.Listings;
using MediaBrowser.Model.LiveTv;
using Moq;
using Moq.Protected;
using Xunit;
-namespace Jellyfin.Server.Implementations.Tests.LiveTv.Listings;
+namespace Jellyfin.LiveTv.Tests.Listings;
public class XmlTvListingsProviderTests
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs b/tests/Jellyfin.LiveTv.Tests/RecordingHelperTests.cs
index f107b1ef97..b4960dc0b3 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
+++ b/tests/Jellyfin.LiveTv.Tests/RecordingHelperTests.cs
@@ -1,9 +1,9 @@
using System;
-using Emby.Server.Implementations.LiveTv.EmbyTV;
+using Jellyfin.LiveTv.EmbyTV;
using MediaBrowser.Controller.LiveTv;
using Xunit;
-namespace Jellyfin.Server.Implementations.Tests.LiveTv
+namespace Jellyfin.LiveTv.Tests
{
public static class RecordingHelperTests
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs
index d4f28f327f..6975d56d9e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
+++ b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs
@@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
-using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
using Jellyfin.Extensions.Json;
+using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
using Xunit;
-namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
+namespace Jellyfin.LiveTv.Tests.SchedulesDirect
{
public class SchedulesDirectDeserializeTests
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/discover.json
index a4ad4ed444..a4ad4ed444 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/discover.json
diff --git a/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/discover.json
index 851f17bb26..851f17bb26 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/discover.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/lineup.json
index 4cb5ebc8ec..4cb5ebc8ec 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/lineup.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
index dd4aa89774..dd4aa89774 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml
index 5a5be79976..5a5be79976 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/headends_response.json
index 015afeecc0..015afeecc0 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/headends_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineup_response.json
index 0720894702..0720894702 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineup_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineups_response.json
index 032a84e59e..032a84e59e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineups_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
index 78166f09a4..78166f09a4 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/programs_response.json
index fe2a944364..fe2a944364 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/programs_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_request.json
index 5ef1bfb1c4..5ef1bfb1c4 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_request.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_response.json
index 4a97e55172..4a97e55172 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_live_response.json
index e5fb64a6fa..e5fb64a6fa 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_live_response.json
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_offline_response.json
index b66a4ed0c4..b66a4ed0c4 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json
+++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_offline_response.json
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 4e8aec9f19..2f21495044 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -7,6 +7,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
using Jellyfin.Api.Models.UserDtos;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Model.Dto;
using Xunit;
@@ -56,7 +57,7 @@ namespace Jellyfin.Server.Integration.Tests
public static async Task<BaseItemDto> GetRootFolderDtoAsync(HttpClient client, Guid userId = default)
{
- if (userId.Equals(default))
+ if (userId.IsEmpty())
{
var userDto = await GetUserDtoAsync(client);
userId = userDto.Id;