aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-abi.yml2
-rw-r--r--.ci/azure-pipelines-main.yml9
-rw-r--r--.ci/azure-pipelines-package.yml28
-rw-r--r--.ci/azure-pipelines-test.yml4
-rw-r--r--.ci/azure-pipelines.yml5
l---------.copr1
l---------.copr/Makefile1
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md49
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml106
-rw-r--r--.github/stale.yml6
-rw-r--r--.github/workflows/automation.yml12
-rw-r--r--.github/workflows/codeql-analysis.yml3
-rw-r--r--.github/workflows/commands.yml2
-rw-r--r--.github/workflows/openapi.yml124
-rw-r--r--.gitignore3
-rw-r--r--.vscode/launch.json4
-rw-r--r--CONTRIBUTORS.md5
-rw-r--r--Directory.Build.props17
-rw-r--r--Dockerfile50
-rw-r--r--Dockerfile.arm45
-rw-r--r--Dockerfile.arm6443
-rw-r--r--DvdLib/DvdLib.csproj5
-rw-r--r--DvdLib/Ifo/Dvd.cs3
-rw-r--r--Emby.Dlna/Configuration/DlnaOptions.cs4
-rw-r--r--Emby.Dlna/ContentDirectory/ContentDirectoryService.cs4
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs1130
-rw-r--r--Emby.Dlna/ContentDirectory/ServerItem.cs19
-rw-r--r--Emby.Dlna/ControlResponse.cs6
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs32
-rw-r--r--Emby.Dlna/DlnaManager.cs113
-rw-r--r--Emby.Dlna/Emby.Dlna.csproj10
-rw-r--r--Emby.Dlna/EventSubscriptionResponse.cs6
-rw-r--r--Emby.Dlna/Eventing/DlnaEventManager.cs29
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs35
-rw-r--r--Emby.Dlna/PlayTo/Device.cs33
-rw-r--r--Emby.Dlna/PlayTo/MediaChangedEventArgs.cs10
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs8
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs9
-rw-r--r--Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs7
-rw-r--r--Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs7
-rw-r--r--Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs7
-rw-r--r--Emby.Dlna/PlayTo/SsdpHttpClient.cs16
-rw-r--r--Emby.Dlna/Server/DescriptionXmlBuilder.cs8
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs10
-rw-r--r--Emby.Dlna/Service/BaseService.cs8
-rw-r--r--Emby.Dlna/Service/ControlErrorHandler.cs6
-rw-r--r--Emby.Drawing/Emby.Drawing.csproj8
-rw-r--r--Emby.Drawing/ImageProcessor.cs4
-rw-r--r--Emby.Naming/Audio/AudioFileParser.cs2
-rw-r--r--Emby.Naming/AudioBook/AudioBookInfo.cs8
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs12
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs4
-rw-r--r--Emby.Naming/Common/NamingOptions.cs96
-rw-r--r--Emby.Naming/Emby.Naming.csproj12
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs11
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs4
-rw-r--r--Emby.Naming/TV/SeriesInfo.cs29
-rw-r--r--Emby.Naming/TV/SeriesPathParser.cs60
-rw-r--r--Emby.Naming/TV/SeriesPathParserResult.cs19
-rw-r--r--Emby.Naming/TV/SeriesResolver.cs49
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs26
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs106
-rw-r--r--Emby.Naming/Video/FileStack.cs29
-rw-r--r--Emby.Naming/Video/FileStackRule.cs48
-rw-r--r--Emby.Naming/Video/StackResolver.cs219
-rw-r--r--Emby.Naming/Video/StubResolver.cs4
-rw-r--r--Emby.Naming/Video/VideoInfo.cs13
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs174
-rw-r--r--Emby.Naming/Video/VideoResolver.cs15
-rw-r--r--Emby.Notifications/Emby.Notifications.csproj6
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs24
-rw-r--r--Emby.Photos/Emby.Photos.csproj6
-rw-r--r--Emby.Photos/PhotoProvider.cs3
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs2
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs10
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs639
-rw-r--r--Emby.Server.Implementations/Archiving/ZipClient.cs113
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs31
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs3
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs48
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs126
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs59
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs6
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs733
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs60
-rw-r--r--Emby.Server.Implementations/Data/SynchronouseMode.cs30
-rw-r--r--Emby.Server.Implementations/Data/TempStoreMode.cs23
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs21
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs146
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs118
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj25
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs8
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs6
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs3
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs8
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs21
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs12
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs7
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs24
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs46
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs82
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs7
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Images/BaseFolderImageProvider.cs67
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs28
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs69
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs45
-rw-r--r--Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs19
-rw-r--r--Emby.Server.Implementations/Images/MusicGenreImageProvider.cs59
-rw-r--r--Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs19
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs33
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs6
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs267
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs7
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs76
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs50
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs9
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs32
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs67
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs33
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs21
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs175
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs10
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs22
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs27
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs (renamed from Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs)6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs136
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs13
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs10
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs52
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs32
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs75
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs53
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs19
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs37
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs14
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs156
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs5
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs122
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs34
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs9
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs692
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs34
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs46
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs40
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs36
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs70
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs46
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs36
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs34
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs58
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs28
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs41
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs156
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs90
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs40
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs66
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs16
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs47
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs41
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs97
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs24
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs35
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs134
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs70
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs17
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs38
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs141
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs25
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs44
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs105
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json22
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json26
-rw-r--r--Emby.Server.Implementations/Localization/Core/as.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json22
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json58
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json98
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json18
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json123
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json35
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/mn.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json60
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json20
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json78
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json31
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/te.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json30
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json24
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json16
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zu.json29
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs293
-rw-r--r--Emby.Server.Implementations/Localization/countries.json2
-rw-r--r--Emby.Server.Implementations/Localization/iso6392.txt3
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs8
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs20
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs12
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs8
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs (renamed from Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs)26
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs28
-rw-r--r--Emby.Server.Implementations/Properties/AssemblyInfo.cs1
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs245
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs25
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs26
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs46
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs12
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs8
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs29
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs33
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs408
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs4
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs5
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs199
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs30
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs14
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs18
-rw-r--r--Emby.Server.Implementations/Sorting/ArtistComparer.cs4
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs14
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs36
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs12
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs14
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs15
-rw-r--r--Emby.Server.Implementations/SyncPlay/Group.cs44
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs8
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs30
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs10
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs13
-rw-r--r--Jellyfin.Api/Attributes/AcceptsFileAttribute.cs4
-rw-r--r--Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/ProducesFileAttribute.cs4
-rw-r--r--Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs2
-rw-r--r--Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs2
-rw-r--r--Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs47
-rw-r--r--Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs2
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs15
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs6
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs6
-rw-r--r--Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs4
-rw-r--r--Jellyfin.Api/BaseJellyfinApiController.cs2
-rw-r--r--Jellyfin.Api/Constants/Policies.cs5
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs53
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ClientLogController.cs80
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs48
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs15
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs25
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs5
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs14
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs10
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs20
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs69
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs10
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs14
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs42
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs63
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs42
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs28
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs29
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs14
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs26
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs91
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs42
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs80
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs2
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs10
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs87
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs12
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs30
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs6
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs63
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs7
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs16
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs46
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs7
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs2
-rw-r--r--Jellyfin.Api/Helpers/AudioHelper.cs29
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs2
-rw-r--r--Jellyfin.Api/Helpers/ClassMigrationHelper.cs71
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs9
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs3
-rw-r--r--Jellyfin.Api/Helpers/HlsHelpers.cs6
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileCopier.cs189
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs125
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs36
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs18
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs37
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj16
-rw-r--r--Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs5
-rw-r--r--Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs22
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs2
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs7
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs4
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs2
-rw-r--r--Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs4
-rw-r--r--Jellyfin.Api/Models/StreamingDtos/StreamState.cs5
-rw-r--r--Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs14
-rw-r--r--Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs4
-rw-r--r--Jellyfin.Data/Dtos/DeviceOptionsDto.cs23
-rw-r--r--Jellyfin.Data/Entities/Security/ApiKey.cs56
-rw-r--r--Jellyfin.Data/Entities/Security/Device.cs107
-rw-r--r--Jellyfin.Data/Entities/Security/DeviceOptions.cs35
-rw-r--r--Jellyfin.Data/Enums/BaseItemKind.cs17
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj10
-rw-r--r--Jellyfin.Data/Queries/ActivityLogQuery.cs12
-rw-r--r--Jellyfin.Data/Queries/DeviceQuery.cs25
-rw-r--r--Jellyfin.Data/Queries/PaginatedQuery.cs18
-rw-r--r--Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj15
-rw-r--r--Jellyfin.Drawing.Skia/SkiaEncoder.cs7
-rw-r--r--Jellyfin.Drawing.Skia/StripCollageBuilder.cs4
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs5
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs6
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs24
-rw-r--r--Jellyfin.Networking/Jellyfin.Networking.csproj6
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs32
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs7
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs244
-rw-r--r--Jellyfin.Server.Implementations/Events/EventManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj20
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDb.cs81
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbProvider.cs14
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs653
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs128
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs121
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs28
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs25
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs24
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs21
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs56
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthenticationManager.cs73
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs (renamed from Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs)189
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs35
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs17
-rw-r--r--Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs22
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs36
-rw-r--r--Jellyfin.Server/Configuration/CorsPolicyProvider.cs4
-rw-r--r--Jellyfin.Server/CoreAppHost.cs56
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs34
-rw-r--r--Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs2
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs144
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj29
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs7
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs5
-rw-r--r--Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs6
-rw-r--r--Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs2
-rw-r--r--Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs13
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs70
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs138
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs129
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs6
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs8
-rw-r--r--Jellyfin.Server/Program.cs76
-rw-r--r--Jellyfin.Server/Startup.cs6
-rw-r--r--Jellyfin.sln16
-rw-r--r--MediaBrowser.Common/Cryptography/CryptoExtensions.cs35
-rw-r--r--MediaBrowser.Common/Extensions/ProcessExtensions.cs2
-rw-r--r--MediaBrowser.Common/FfmpegException.cs (renamed from MediaBrowser.MediaEncoding/FfmpegException.cs)2
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs11
-rw-r--r--MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs26
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj12
-rw-r--r--MediaBrowser.Common/Net/IPHost.cs61
-rw-r--r--MediaBrowser.Common/Plugins/BasePluginOfT.cs4
-rw-r--r--MediaBrowser.Common/Providers/ProviderIdParsers.cs8
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs24
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs4
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs12
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemInfo.cs2
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemResult.cs7
-rw-r--r--MediaBrowser.Controller/Channels/IHasFolderAttributes.cs2
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelFeatures.cs2
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterManager.cs2
-rw-r--r--MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs31
-rw-r--r--MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs23
-rw-r--r--MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs9
-rw-r--r--MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs2
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs40
-rw-r--r--MediaBrowser.Controller/Dlna/IDlnaManager.cs13
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs9
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/ImageStream.cs11
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs1
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs43
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs55
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs52
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs66
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs39
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs1430
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs7
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs12
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs45
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs56
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs12
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs9
-rw-r--r--MediaBrowser.Controller/Entities/IHasShares.cs2
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs4
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs67
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs164
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs62
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs71
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs44
-rw-r--r--MediaBrowser.Controller/Entities/PersonInfo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs48
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs36
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs150
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs92
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs38
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs8
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs14
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs46
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs88
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs102
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs268
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs38
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs73
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs4
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs45
-rw-r--r--MediaBrowser.Controller/Library/IDirectStreamProvider.cs19
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs141
-rw-r--r--MediaBrowser.Controller/Library/ILiveStream.cs5
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs49
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceProvider.cs6
-rw-r--r--MediaBrowser.Controller/Library/IMetadataSaver.cs1
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs14
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs13
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs30
-rw-r--r--MediaBrowser.Controller/Library/IUserViewManager.cs21
-rw-r--r--MediaBrowser.Controller/Library/ItemChangeEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs77
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs2
-rw-r--r--MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs2
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs39
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs3
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs126
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs139
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerInfo.cs12
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs18
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs682
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs445
-rw-r--r--MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs23
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs7
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs67
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs8
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs16
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs32
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs3
-rw-r--r--MediaBrowser.Controller/Net/IAuthorizationContext.cs9
-rw-r--r--MediaBrowser.Controller/Net/ISessionContext.cs9
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs8
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs1
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs82
-rw-r--r--MediaBrowser.Controller/Providers/AlbumInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ArtistInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs4
-rw-r--r--MediaBrowser.Controller/Providers/EpisodeInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs4
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs15
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs22
-rw-r--r--MediaBrowser.Controller/Providers/ItemInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/ItemLookupInfo.cs4
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs5
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs2
-rw-r--r--MediaBrowser.Controller/Providers/SeasonInfo.cs2
-rw-r--r--MediaBrowser.Controller/Providers/SongInfo.cs12
-rw-r--r--MediaBrowser.Controller/QuickConnect/IQuickConnect.cs65
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs20
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs53
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationManager.cs33
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationRepository.cs39
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs6
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs85
-rw-r--r--MediaBrowser.Controller/Sorting/SortExtensions.cs5
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs21
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs18
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupMember.cs23
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs11
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs6
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs6
-rw-r--r--MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs6
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs18
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs124
-rw-r--r--MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs5
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs31
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj6
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs71
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs52
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs9
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs2
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs106
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs344
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj16
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs47
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs722
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs49
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs13
-rw-r--r--MediaBrowser.Model/Activity/ActivityLogEntry.cs27
-rw-r--r--MediaBrowser.Model/Branding/BrandingOptions.cs5
-rw-r--r--MediaBrowser.Model/Channels/ChannelFeatures.cs8
-rw-r--r--MediaBrowser.Model/Channels/ChannelInfo.cs32
-rw-r--r--MediaBrowser.Model/Channels/ChannelQuery.cs5
-rw-r--r--MediaBrowser.Model/ClientLog/ClientLogEvent.cs75
-rw-r--r--MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs7
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs18
-rw-r--r--MediaBrowser.Model/Configuration/MediaPathInfo.cs14
-rw-r--r--MediaBrowser.Model/Configuration/MetadataOptions.cs2
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs229
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs5
-rw-r--r--MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs3
-rw-r--r--MediaBrowser.Model/Cryptography/Constants.cs (renamed from MediaBrowser.Common/Cryptography/Constants.cs)11
-rw-r--r--MediaBrowser.Model/Cryptography/ICryptoProvider.cs13
-rw-r--r--MediaBrowser.Model/Cryptography/PasswordHash.cs (renamed from MediaBrowser.Common/Cryptography/PasswordHash.cs)2
-rw-r--r--MediaBrowser.Model/Devices/DeviceInfo.cs5
-rw-r--r--MediaBrowser.Model/Devices/DeviceOptions.cs9
-rw-r--r--MediaBrowser.Model/Devices/DeviceQuery.cs21
-rw-r--r--MediaBrowser.Model/Dlna/CodecProfile.cs4
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs4
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs4
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs16
-rw-r--r--MediaBrowser.Model/Dlna/DeviceIdentification.cs1
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs12
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs1
-rw-r--r--MediaBrowser.Model/Dlna/DlnaMaps.cs14
-rw-r--r--MediaBrowser.Model/Dlna/ITranscoderSupport.cs1
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfile.cs2
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs1
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs6
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs13
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs65
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs89
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs7
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleProfile.cs4
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs1
-rw-r--r--MediaBrowser.Model/Dto/DisplayPreferencesDto.cs (renamed from Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs)10
-rw-r--r--MediaBrowser.Model/Entities/DisplayPreferencesDto.cs107
-rw-r--r--MediaBrowser.Model/Entities/ImageType.cs4
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs81
-rw-r--r--MediaBrowser.Model/Entities/PersonType.cs40
-rw-r--r--MediaBrowser.Model/Entities/VirtualFolderInfo.cs1
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs16
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs16
-rw-r--r--MediaBrowser.Model/IO/AsyncFile.cs45
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs4
-rw-r--r--MediaBrowser.Model/IO/IZipClient.cs56
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj19
-rw-r--r--MediaBrowser.Model/MediaInfo/AudioCodec.cs27
-rw-r--r--MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs16
-rw-r--r--MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs58
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleFormat.cs2
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs164
-rw-r--r--MediaBrowser.Model/Net/NetworkShareType.cs33
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs8
-rw-r--r--MediaBrowser.Model/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdInfo.cs4
-rw-r--r--MediaBrowser.Model/Querying/ItemFields.cs3
-rw-r--r--MediaBrowser.Model/Querying/LatestItemsQuery.cs3
-rw-r--r--MediaBrowser.Model/QuickConnect/QuickConnectResult.cs62
-rw-r--r--MediaBrowser.Model/QuickConnect/QuickConnectState.cs23
-rw-r--r--MediaBrowser.Model/Search/SearchQuery.cs9
-rw-r--r--MediaBrowser.Model/Session/BrowseRequest.cs5
-rw-r--r--MediaBrowser.Model/Session/HardwareEncodingType.cs48
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs3
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs10
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs2
-rw-r--r--MediaBrowser.Model/Users/PinRedeemResult.cs5
-rw-r--r--MediaBrowser.Model/Users/UserActionType.cs9
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs2
-rw-r--r--MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs8
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs34
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs222
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs33
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs54
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs6
-rw-r--r--MediaBrowser.Providers/Manager/RefreshResult.cs2
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj14
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs40
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioResolver.cs176
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs248
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs76
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs37
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs12
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs52
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs129
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Music/AlbumInfoExtensions.cs12
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs4
-rw-r--r--MediaBrowser.Providers/Music/ImvdbId.cs2
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs)7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs)30
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs)7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs)27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs119
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs426
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs (renamed from MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs)27
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs)6
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs)4
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs37
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs56
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs226
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs191
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html58
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs43
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs (renamed from MediaBrowser.Providers/Studios/StudiosImageProvider.cs)24
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs41
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs44
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs50
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html137
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs44
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs60
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs26
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs41
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs203
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs7
-rw-r--r--MediaBrowser.Providers/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.Providers/Studios/StudioMetadataService.cs3
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs33
-rw-r--r--MediaBrowser.Providers/TV/EpisodeMetadataService.cs16
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs14
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs11
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalId.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs4
-rw-r--r--MediaBrowser.XbmcMetadata/EntryPoint.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj6
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs247
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs103
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs20
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs18
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs2
-rw-r--r--README.md17
-rw-r--r--RSSDP/DisposableManagedObjectBase.cs5
-rw-r--r--RSSDP/HttpParserBase.cs6
-rw-r--r--RSSDP/HttpRequestParser.cs4
-rw-r--r--RSSDP/HttpResponseParser.cs4
-rw-r--r--RSSDP/RSSDP.csproj6
-rw-r--r--RSSDP/SsdpCommunicationsServer.cs3
-rw-r--r--RSSDP/SsdpDevice.cs13
-rw-r--r--RSSDP/SsdpDeviceLocator.cs2
-rw-r--r--RSSDP/SsdpDevicePublisher.cs30
-rwxr-xr-xbump_version31
-rwxr-xr-xdebian/bin/restart.sh40
-rw-r--r--debian/conf/jellyfin5
-rw-r--r--debian/conf/jellyfin-sudoers6
-rw-r--r--debian/control4
-rw-r--r--debian/jellyfin.service38
-rwxr-xr-xdebian/rules2
-rw-r--r--deployment/Dockerfile.centos.amd6422
-rw-r--r--deployment/Dockerfile.debian.amd6417
-rw-r--r--deployment/Dockerfile.debian.arm6432
-rw-r--r--deployment/Dockerfile.debian.armhf33
-rw-r--r--deployment/Dockerfile.docker.amd644
-rw-r--r--deployment/Dockerfile.docker.arm644
-rw-r--r--deployment/Dockerfile.docker.armhf4
-rw-r--r--deployment/Dockerfile.fedora.amd6419
-rw-r--r--deployment/Dockerfile.linux.amd6417
-rw-r--r--deployment/Dockerfile.linux.amd64-musl17
-rw-r--r--deployment/Dockerfile.linux.arm6417
-rw-r--r--deployment/Dockerfile.linux.armhf17
-rw-r--r--deployment/Dockerfile.macos17
-rw-r--r--deployment/Dockerfile.portable17
-rw-r--r--deployment/Dockerfile.ubuntu.amd6417
-rw-r--r--deployment/Dockerfile.ubuntu.arm6453
-rw-r--r--deployment/Dockerfile.ubuntu.armhf53
-rw-r--r--deployment/Dockerfile.windows.amd6417
-rwxr-xr-xdeployment/build.centos.amd6419
-rwxr-xr-xdeployment/build.debian.amd644
-rwxr-xr-xdeployment/build.debian.arm644
-rwxr-xr-xdeployment/build.debian.armhf4
-rwxr-xr-xdeployment/build.fedora.amd6419
-rwxr-xr-xdeployment/build.portable2
-rwxr-xr-xdeployment/build.ubuntu.amd644
-rwxr-xr-xdeployment/build.ubuntu.arm644
-rwxr-xr-xdeployment/build.ubuntu.armhf4
-rw-r--r--deployment/unraid/docker-templates/README.md2
-rw-r--r--fedora/Makefile75
-rw-r--r--fedora/jellyfin-server-lowports.conf4
-rw-r--r--fedora/jellyfin.service2
-rw-r--r--fedora/jellyfin.spec25
-rw-r--r--fedora/jellyfin.sudoers7
-rwxr-xr-xfedora/restart.sh40
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj7
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Program.cs30
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt1
-rw-r--r--jellyfin.ruleset42
-rw-r--r--src/Jellyfin.Extensions/AlphanumericComparator.cs (renamed from MediaBrowser.Controller/Sorting/AlphanumComparator.cs)15
-rw-r--r--src/Jellyfin.Extensions/CopyToExtensions.cs (renamed from MediaBrowser.Common/Extensions/CopyToExtensions.cs)2
-rw-r--r--src/Jellyfin.Extensions/DictionaryExtensions.cs64
-rw-r--r--src/Jellyfin.Extensions/EnumerableExtensions.cs (renamed from MediaBrowser.Common/Extensions/EnumerableExtensions.cs)4
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj37
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs)4
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs)7
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs (renamed from MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs)2
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs)4
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs)12
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs30
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs (renamed from MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs)10
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs)13
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs)22
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs (renamed from MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs)2
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs)7
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs (renamed from MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs)2
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonStringConverter.cs)15
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs (renamed from MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs)2
-rw-r--r--src/Jellyfin.Extensions/Json/JsonDefaults.cs (renamed from MediaBrowser.Common/Json/JsonDefaults.cs)4
-rw-r--r--src/Jellyfin.Extensions/ReadOnlyListExtension.cs61
-rw-r--r--src/Jellyfin.Extensions/ShuffleExtensions.cs (renamed from MediaBrowser.Common/Extensions/ShuffleExtensions.cs)6
-rw-r--r--src/Jellyfin.Extensions/SplitStringExtensions.cs (renamed from MediaBrowser.Common/Extensions/SplitStringExtensions.cs)38
-rw-r--r--src/Jellyfin.Extensions/StreamExtensions.cs (renamed from MediaBrowser.Common/Extensions/StreamExtensions.cs)2
-rw-r--r--src/Jellyfin.Extensions/StringBuilderExtensions.cs (renamed from MediaBrowser.Common/Extensions/StringBuilderExtensions.cs)4
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs65
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs4
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs57
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs39
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs54
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj13
-rw-r--r--tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs40
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj11
-rw-r--r--tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs88
-rw-r--r--tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs19
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj9
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj9
-rw-r--r--tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs (renamed from tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs)9
-rw-r--r--tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs59
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj35
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs)5
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs)6
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs)6
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs)4
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs (renamed from tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs)3
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs)4
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs)6
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs)6
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs (renamed from tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs)6
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs (renamed from tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs)6
-rw-r--r--tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs (renamed from tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs)7
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs41
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs36
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs24
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs8
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj13
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs138
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs22
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs12
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs83
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json144
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json147
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json260
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt11
-rw-r--r--tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs (renamed from tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs)68
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs150
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj11
-rw-r--r--tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs159
-rw-r--r--tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs27
-rw-r--r--tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj9
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs5
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs28
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs28
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs3
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs16
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs77
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs124
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/StackTests.cs100
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs228
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs117
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj11
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs52
-rw-r--r--tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj15
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs597
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs156
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs45
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs126
-rw-r--r--tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs (renamed from tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs)3
-rw-r--r--tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg0
-rw-r--r--tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg0
-rw-r--r--tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs13
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs103
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs6
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj9
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs17
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs232
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs32
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs12
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs63
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs240
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs179
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs124
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs164
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json1
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zipbin0 -> 162 bytes
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs63
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs30
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs9
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs4
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs134
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs61
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs117
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs12
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs17
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs1
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj15
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs15
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs32
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs12
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj15
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs95
-rw-r--r--tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs3
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj9
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs8
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs17
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Fanart.nfo33
980 files changed, 22579 insertions, 15788 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml
index 8d0737b66..cf74a4201 100644
--- a/.ci/azure-pipelines-abi.yml
+++ b/.ci/azure-pipelines-abi.yml
@@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
- default: 5.0.103
+ default: 6.0.x
jobs:
- job: CompatibilityCheck
diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml
index 4bc72f9eb..b7112ba24 100644
--- a/.ci/azure-pipelines-main.yml
+++ b/.ci/azure-pipelines-main.yml
@@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
- DotNetSdkVersion: 5.0.103
+ DotNetSdkVersion: 6.0.x
jobs:
- job: Build
@@ -91,3 +91,10 @@ jobs:
inputs:
targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
+
+ - task: PublishPipelineArtifact@1
+ displayName: 'Publish Artifact Extensions'
+ condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release'))
+ inputs:
+ targetPath: '$(build.ArtifactStagingDirectory)/Jellyfin.Server/Jellyfin.Extensions.dll'
+ artifactName: 'Jellyfin.Extensions'
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 543fd7fc6..89f7137fd 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -39,6 +39,14 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+ - script: './bump-version $(JellyfinVersion)'
+ displayName: Bump internal version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
@@ -80,6 +88,14 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
+ - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+ displayName: Set release version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+ - script: './bump-version $(JellyfinVersion)'
+ displayName: Bump internal version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
@@ -127,6 +143,10 @@ jobs:
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ - script: './bump-version $(JellyfinVersion)'
+ displayName: Bump internal version (stable)
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@@ -181,7 +201,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
- commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+ commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
- job: PublishNuget
displayName: 'Publish NuGet packages'
@@ -195,10 +215,10 @@ jobs:
steps:
- task: UseDotNet@2
- displayName: 'Use .NET 5.0 sdk'
+ displayName: 'Use .NET 6.0 sdk'
inputs:
packageType: 'sdk'
- version: '5.0.x'
+ version: '6.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages'
@@ -211,6 +231,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
@@ -225,6 +246,7 @@ jobs:
MediaBrowser.Controller/MediaBrowser.Controller.csproj
MediaBrowser.Model/MediaBrowser.Model.csproj
Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
custom: 'pack'
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index 7838b3b02..cc94dc2c5 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
- default: 5.0.103
+ default: 6.0.x
jobs:
- job: Test
@@ -94,5 +94,5 @@ jobs:
displayName: 'Publish OpenAPI Artifact'
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
inputs:
- targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net5.0/openapi.json"
+ targetPath: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json"
artifactName: 'OpenAPI Spec'
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index c028b6e3e..19c9caacb 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -5,8 +5,6 @@ variables:
value: 'tests/**/*Tests.csproj'
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
-- name: DotNetSdkVersion
- value: 5.0.103
pr:
autoCancel: true
@@ -57,6 +55,9 @@ jobs:
Common:
NugetPackageName: Jellyfin.Common
AssemblyFileName: MediaBrowser.Common.dll
+ Extensions:
+ NugetPackageName: Jellyfin.Extensions
+ AssemblyFileName: Jellyfin.Extensions.dll
LinuxImage: 'ubuntu-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
diff --git a/.copr b/.copr
new file mode 120000
index 000000000..100fe0cd7
--- /dev/null
+++ b/.copr
@@ -0,0 +1 @@
+fedora \ No newline at end of file
diff --git a/.copr/Makefile b/.copr/Makefile
deleted file mode 120000
index ec3c90dfd..000000000
--- a/.copr/Makefile
+++ /dev/null
@@ -1 +0,0 @@
-../fedora/Makefile \ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 12f1f5ed5..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-name: Bug report
-about: Create a bug report
-title: ''
-labels: bug
-assignees: ''
-
----
-
-**Describe the bug**
-<!-- A clear and concise description of what the bug is. -->
-
-**System (please complete the following information):**
- - OS: [e.g. Debian, Windows]
- - Virtualization: [e.g. Docker, KVM, LXC]
- - Clients: [Browser, Android, Fire Stick, etc.]
- - Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- - Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- - Playback: [Direct Play, Remux, Direct Stream, Transcode]
- - Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- - Reverse Proxy: [e.g. none, nginx, apache, etc.]
- - Base URL: [e.g. none, yes: /example]
- - Networking: [e.g. Host, Bridge/NAT]
- - Storage: [e.g. local, NFS, cloud]
-
-**To Reproduce**
-<!-- Steps to reproduce the behavior: -->
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-<!-- A clear and concise description of what you expected to happen. -->
-
-**Server Logs**
-<!-- Please paste any log errors. -->
-
-**FFmpeg Logs**
-<!-- Please paste any log errors. -->
-
-**Browser Console Logs**
-<!-- Please paste any log errors. -->
-
-**Screenshots**
-<!-- If applicable, add screenshots to help explain your problem. -->
-
-**Additional context**
-<!-- Add any other context about the problem here. -->
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
new file mode 100644
index 000000000..63e0f0e22
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -0,0 +1,106 @@
+name: Issue Report
+description: File an issue report
+title: "[Issue]: "
+labels: [bug, triage]
+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).
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: Please describe your bug
+ 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...
+ validations:
+ required: true
+ - type: dropdown
+ id: version
+ attributes:
+ label: Jellyfin Version
+ description: What version of Jellyfin are you running?
+ options:
+ - 10.7.7
+ - 10.7.z
+ - 10.6.4
+ - Other
+ validations:
+ required: true
+ - type: input
+ id: version-other
+ attributes:
+ label: "if other:"
+ placeholder: Other
+ - type: textarea
+ attributes:
+ label: Environment
+ description: |
+ Examples:
+ - **OS**: [e.g. Debian, Windows]
+ - **Virtualization**: [e.g. Docker, KVM, LXC]
+ - **Clients**: [Browser, Android, Fire Stick, etc.]
+ - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
+ - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
+ - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
+ - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+ - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
+ - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
+ - **Base URL**: [e.g. none, yes: /example]
+ - **Networking**: [e.g. Host, Bridge/NAT]
+ - **Storage**: [e.g. local, NFS, cloud]
+ value: |
+ - OS:
+ - Virtualization:
+ - Clients:
+ - Browser:
+ - FFmpeg Version:
+ - Playback Method:
+ - Hardware Acceleration:
+ - Plugins:
+ - Reverse Proxy:
+ - Base URL:
+ - Networking:
+ - Storage:
+ render: markdown
+ - type: textarea
+ id: logs
+ attributes:
+ label: Jellyfin logs
+ 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
+ - type: textarea
+ id: ffmpeg-logs
+ attributes:
+ label: FFmpeg logs
+ description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+ placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+ render: shell
+ - type: textarea
+ id: browserlogs
+ attributes:
+ label: Please attach any browser or client logs here
+ placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Please attach any screenshots here
+ placeholder: Images can be pasted directly into the textbox and will be hosted by github.
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Code of Conduct
+ description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
+ options:
+ - label: I agree to follow this project's Code of Conduct
+ required: true
diff --git a/.github/stale.yml b/.github/stale.yml
index 05892c44d..cba9c33b2 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -17,9 +17,13 @@ staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
-
+
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
+
+# Disable automatic closing of pull requests
+pulls:
+ daysUntilClose: false
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 8da2349c8..20294843d 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -11,6 +11,7 @@ jobs:
label:
name: Labeling
runs-on: ubuntu-latest
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@v2.0.1
@@ -22,9 +23,10 @@ jobs:
project:
name: Project board
runs-on: ubuntu-latest
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
- uses: alex-page/github-project-automation-plus@v0.7.1
+ uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -33,7 +35,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
- uses: alex-page/github-project-automation-plus@v0.7.1
+ uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@@ -42,7 +44,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
- uses: alex-page/github-project-automation-plus@v0.7.1
+ uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
@@ -56,7 +58,7 @@ jobs:
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
- uses: alex-page/github-project-automation-plus@v0.7.1
+ uses: alex-page/github-project-automation-plus@v0.8.1
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@@ -65,7 +67,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
- uses: alex-page/github-project-automation-plus@v0.7.1
+ uses: alex-page/github-project-automation-plus@v0.8.1
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3e456f909..ea1d30cdf 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,7 +24,8 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: '5.0.x'
+ dotnet-version: '6.0.x'
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index e0b91ecee..af4d8beb9 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Automatic Rebase
- uses: cirrus-actions/rebase@1.4
+ uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
new file mode 100644
index 000000000..3e9346840
--- /dev/null
+++ b/.github/workflows/openapi.yml
@@ -0,0 +1,124 @@
+name: OpenAPI
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+
+jobs:
+ openapi-head:
+ name: OpenAPI - HEAD
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '6.0.x'
+ - 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@v2
+ with:
+ name: openapi-head
+ retention-days: 14
+ if-no-files-found: error
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+ openapi-base:
+ name: OpenAPI - BASE
+ if: ${{ github.base_ref != '' }}
+ runs-on: ubuntu-latest
+ permissions: read-all
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.base_ref }}
+ - name: Setup .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: '6.0.x'
+ - 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@v2
+ with:
+ name: openapi-base
+ retention-days: 14
+ if-no-files-found: error
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+ openapi-diff:
+ name: OpenAPI - Difference
+ if: ${{ github.event_name == 'pull_request_target' }}
+ runs-on: ubuntu-latest
+ needs:
+ - openapi-head
+ - openapi-base
+ steps:
+ - name: Download openapi-head
+ uses: actions/download-artifact@v2
+ with:
+ name: openapi-head
+ path: openapi-head
+ - name: Download openapi-base
+ uses: actions/download-artifact@v2
+ with:
+ name: openapi-base
+ path: openapi-base
+ - name: Workaround openapi-diff issue
+ run: |
+ sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
+ sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
+ - name: Calculate OpenAPI difference
+ uses: docker://openapitools/openapi-diff
+ continue-on-error: true
+ with:
+ args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
+ - id: read-diff
+ name: Read openapi-diff output
+ run: |
+ body=$(cat openapi-changes.md)
+ body="${body//'%'/'%25'}"
+ body="${body//$'\n'/'%0A'}"
+ body="${body//$'\r'/'%0D'}"
+ echo ::set-output name=body::$body
+ - name: Find difference comment
+ uses: peter-evans/find-comment@v1
+ 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@v1.4.5
+ if: ${{ steps.read-diff.outputs.body != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+ <!--openapi-diff-workflow-comment-->
+ <details>
+ <summary>Changes in OpenAPI specification found. Expand to see details.</summary>
+
+ ${{ steps.read-diff.outputs.body }}
+
+ </details>
+ - name: Edit difference comment (unchanged)
+ uses: peter-evans/create-or-update-comment@v1.4.5
+ if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find-comment.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+ <!--openapi-diff-workflow-comment-->
+
+ No changes to OpenAPI specification found. See history of this comment for previous changes.
diff --git a/.gitignore b/.gitignore
index 252210e57..c2ae76c1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -278,3 +278,6 @@ web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated
+
+# Omnisharp crash logs
+mono_crash.*.json
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e55ea2248..b82956a72 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net5.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net6.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index b44961bf8..d52e13324 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -46,6 +46,7 @@
- [fruhnow](https://github.com/fruhnow)
- [geilername](https://github.com/geilername)
- [gnattu](https://github.com/gnattu)
+ - [GodTamIt](https://github.com/GodTamIt)
- [grafixeyehero](https://github.com/grafixeyehero)
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
@@ -148,6 +149,8 @@
- [skyfrk](https://github.com/skyfrk)
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
+ - [MBR-0001](https://github.com/MBR-0001)
+ - [jonas-resch](https://github.com/jonas-resch)
# Emby Contributors
@@ -212,3 +215,5 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
+ - [lbenini](https://github.com/lbenini)
+ - [gnuyent](https://github.com/gnuyent)
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 000000000..d243cde2b
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,17 @@
+<Project>
+ <!-- Sets defaults for all projects in the repo -->
+
+ <PropertyGroup>
+ <Nullable>enable</Nullable>
+ <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+ </PropertyGroup>
+
+</Project>
diff --git a/Dockerfile b/Dockerfile
index 4e2d06b82..e133c0819 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,8 @@
-ARG DOTNET_VERSION=5.0
+# DESIGNED FOR BUILDING ON AMD64 ONLY
+#####################################
+# Requires binfm_misc registration
+# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
@@ -8,15 +12,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-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;DebugType=none"
-
-FROM debian:buster-slim
+FROM debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -25,19 +21,17 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
# https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=20.3.2
-ARG IGC_VERSION=1.0.5435
-ARG NEO_VERSION=20.46.18421
-ARG LEVEL_ZERO_VERSION=1.0.18421
+ARG GMMLIB_VERSION=21.2.1
+ARG IGC_VERSION=1.0.8517
+ARG NEO_VERSION=21.35.20826
+ARG LEVEL_ZERO_VERSION=1.2.20826
# 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 apt-transport-https \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& 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 \
@@ -68,14 +62,32 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
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;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 25a0de7db..a46fa331d 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -1,8 +1,8 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
@@ -13,19 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-
-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;DebugType=none"
-
-
FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:buster-slim
+FROM arm32v7/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -35,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
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 - && \
@@ -53,7 +44,7 @@ RUN apt-get update \
vainfo \
libva2 \
locales \
- && apt-get remove curl gnupg -y \
+ && apt-get remove gnupg -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -61,17 +52,33 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
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;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index c9f19c5a3..1279c47f8 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -1,8 +1,8 @@
-# DESIGNED FOR BUILDING ON AMD64 ONLY
+# DESIGNED FOR BUILDING ON ARM64 ONLY
#####################################
# Requires binfm_misc registration
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
-ARG DOTNET_VERSION=5.0
+ARG DOTNET_VERSION=6.0
FROM node:lts-alpine as web-builder
@@ -13,18 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
-
-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;DebugType=none"
-
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:buster-slim
+FROM arm64v8/debian:stable-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@@ -34,6 +24,8 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
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 \
@@ -43,6 +35,7 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
libomxil-bellagio0 \
libomxil-bellagio-bin \
locales \
+ curl \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -50,17 +43,33 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
-COPY --from=builder /jellyfin /jellyfin
-COPY --from=web-builder /dist /jellyfin/jellyfin-web
-
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
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;DebugType=none"
+
+FROM app
+
+ENV HEALTHCHECK_URL=http://localhost:8096/health
+
+COPY --from=builder /jellyfin /jellyfin
+COPY --from=web-builder /dist /jellyfin/jellyfin-web
+
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \
"--datadir", "/config", \
"--cachedir", "/cache", \
"--ffmpeg", "/usr/bin/ffmpeg"]
+
+HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
+ CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
index 7bbd9acf8..755d29160 100644
--- a/DvdLib/DvdLib.csproj
+++ b/DvdLib/DvdLib.csproj
@@ -10,10 +10,11 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <AnalysisMode>AllDisabledByDefault</AnalysisMode>
+ <Nullable>disable</Nullable>
</PropertyGroup>
</Project>
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
index b4a11ed5d..7f8ece47d 100644
--- a/DvdLib/Ifo/Dvd.cs
+++ b/DvdLib/Ifo/Dvd.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
@@ -76,7 +77,7 @@ namespace DvdLib.Ifo
private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
{
- var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum);
+ var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index 5ceeb5530..91fac4bef 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
namespace Emby.Dlna.Configuration
@@ -74,7 +72,7 @@ namespace Emby.Dlna.Configuration
/// <summary>
/// Gets or sets the default user account that the dlna server uses.
/// </summary>
- public string DefaultUserId { get; set; }
+ public string? DefaultUserId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether playTo device profiles should be created.
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
index 7b8c50440..9020dea99 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -140,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
/// </summary>
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
/// <returns>The <see cref="User"/>.</returns>
- private User GetUser(DeviceProfile profile)
+ private User? GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))
{
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 27c5b2268..fde3f2f89 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -18,23 +18,16 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
@@ -50,7 +43,6 @@ namespace Emby.Dlna.ContentDirectory
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataManager;
- private readonly IServerConfigurationManager _config;
private readonly User _user;
private readonly IUserViewManager _userViewManager;
private readonly ITVSeriesManager _tvSeriesManager;
@@ -104,7 +96,6 @@ namespace Emby.Dlna.ContentDirectory
_userViewManager = userViewManager;
_tvSeriesManager = tvSeriesManager;
_profile = profile;
- _config = config;
_didlBuilder = new DidlBuilder(
profile,
@@ -288,21 +279,14 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The xml feature list.</returns>
private static string WriteFeatureListXml()
{
- // TODO: clean this up
- var builder = new StringBuilder();
-
- builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
- builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
-
- builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
- builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
- builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
- builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
- builder.Append("</Feature>");
-
- builder.Append("</Features>");
-
- return builder.ToString();
+ return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
+ + "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
+ + "<container id=\"0\" type=\"object.item.imageItem\"/>"
+ + "<container id=\"0\" type=\"object.item.audioItem\"/>"
+ + "<container id=\"0\" type=\"object.item.videoItem\"/>"
+ + "</Feature>"
+ + "</Features>";
}
/// <summary>
@@ -337,75 +321,73 @@ namespace Emby.Dlna.ContentDirectory
int totalCount;
+ var settings = new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment
+ };
+
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ using (var writer = XmlWriter.Create(builder, settings))
{
- var settings = new XmlWriterSettings()
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
+ writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+ writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+ writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+ writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+ DidlBuilder.WriteXmlRootAttributes(_profile, writer);
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+ var serverItem = GetItemFromObjectId(id);
+ var item = serverItem.Item;
- var serverItem = GetItemFromObjectId(id);
- var item = serverItem.Item;
+ if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
+ {
+ totalCount = 1;
- if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
+ if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
{
- totalCount = 1;
-
- if (item.IsDisplayedAsFolder || serverItem.StubType.HasValue)
- {
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
-
- _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
- }
+ var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- provided++;
+ _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id);
}
else
{
- var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
- totalCount = childrenResult.TotalRecordCount;
+ _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter);
+ }
+
+ provided++;
+ }
+ else
+ {
+ var childrenResult = GetUserItems(item, serverItem.StubType, _user, sortCriteria, start, requestedCount);
+ totalCount = childrenResult.TotalRecordCount;
+
+ provided = childrenResult.Items.Count;
- provided = childrenResult.Items.Count;
+ foreach (var i in childrenResult.Items)
+ {
+ var childItem = i.Item;
+ var displayStubType = i.StubType;
+
+ if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
+ {
+ var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
+ .TotalRecordCount;
- foreach (var i in childrenResult.Items)
+ _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
+ }
+ else
{
- var childItem = i.Item;
- var displayStubType = i.StubType;
-
- if (childItem.IsDisplayedAsFolder || displayStubType.HasValue)
- {
- var childCount = GetUserItems(childItem, displayStubType, _user, sortCriteria, null, 0)
- .TotalRecordCount;
-
- _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
- }
+ _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter);
}
}
-
- writer.WriteFullEndElement();
}
+ writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -456,53 +438,46 @@ namespace Emby.Dlna.ContentDirectory
}
QueryResult<BaseItem> childrenResult;
+ var settings = new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment
+ };
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
+ using (var writer = XmlWriter.Create(builder, settings))
{
- var settings = new XmlWriterSettings()
- {
- Encoding = Encoding.UTF8,
- CloseOutput = false,
- OmitXmlDeclaration = true,
- ConformanceLevel = ConformanceLevel.Fragment
- };
-
- using (var writer = XmlWriter.Create(builder, settings))
- {
- writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+ writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
+ writer.WriteAttributeString("xmlns", "dc", null, NsDc);
+ writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
+ writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
- writer.WriteAttributeString("xmlns", "dc", null, NsDc);
- writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
- writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
+ DidlBuilder.WriteXmlRootAttributes(_profile, writer);
- DidlBuilder.WriteXmlRootAttributes(_profile, writer);
+ var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
- var serverItem = GetItemFromObjectId(sparams["ContainerID"]);
+ var item = serverItem.Item;
- var item = serverItem.Item;
-
- childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
-
- var dlnaOptions = _config.GetDlnaConfiguration();
-
- foreach (var i in childrenResult.Items)
+ childrenResult = GetChildrenSorted(item, _user, searchCriteria, sortCriteria, start, requestedCount);
+ foreach (var i in childrenResult.Items)
+ {
+ if (i.IsDisplayedAsFolder)
{
- if (i.IsDisplayedAsFolder)
- {
- var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
- .TotalRecordCount;
+ var childCount = GetChildrenSorted(i, _user, searchCriteria, sortCriteria, null, 0)
+ .TotalRecordCount;
- _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
- }
- else
- {
- _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
- }
+ _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter);
+ }
+ else
+ {
+ _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter);
}
-
- writer.WriteFullEndElement();
}
+ writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -525,48 +500,38 @@ namespace Emby.Dlna.ContentDirectory
{
var folder = (Folder)item;
- var sortOrders = folder.IsPreSorted
- ? Array.Empty<(string, SortOrder)>()
- : new[] { (ItemSortBy.SortName, sort.SortOrder) };
-
string[] mediaTypes = Array.Empty<string>();
bool? isFolder = null;
- if (search.SearchType == SearchType.Audio)
- {
- mediaTypes = new[] { MediaType.Audio };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Video)
+ switch (search.SearchType)
{
- mediaTypes = new[] { MediaType.Video };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Image)
- {
- mediaTypes = new[] { MediaType.Photo };
- isFolder = false;
- }
- else if (search.SearchType == SearchType.Playlist)
- {
- // items = items.OfType<Playlist>();
- isFolder = true;
- }
- else if (search.SearchType == SearchType.MusicAlbum)
- {
- // items = items.OfType<MusicAlbum>();
- isFolder = true;
+ case SearchType.Audio:
+ mediaTypes = new[] { MediaType.Audio };
+ isFolder = false;
+ break;
+ case SearchType.Video:
+ mediaTypes = new[] { MediaType.Video };
+ isFolder = false;
+ break;
+ case SearchType.Image:
+ mediaTypes = new[] { MediaType.Photo };
+ isFolder = false;
+ break;
+ case SearchType.Playlist:
+ case SearchType.MusicAlbum:
+ isFolder = true;
+ break;
}
return folder.GetItems(new InternalItemsQuery
{
Limit = limit,
StartIndex = startIndex,
- OrderBy = sortOrders,
+ OrderBy = GetOrderBy(sort, folder.IsPreSorted),
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@@ -594,52 +559,49 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
{
- if (item is MusicGenre)
- {
- return GetMusicGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
+ switch (item)
+ {
+ case MusicGenre:
+ return GetMusicGenreItems(item, user, sort, startIndex, limit);
+ case MusicArtist:
+ return GetMusicArtistItems(item, user, sort, startIndex, limit);
+ case Genre:
+ return GetGenreItems(item, user, sort, startIndex, limit);
}
- if (item is MusicArtist)
+ if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
{
- return GetMusicArtistItems(item, Guid.Empty, user, sort, startIndex, limit);
- }
-
- if (item is Genre)
- {
- return GetGenreItems(item, Guid.Empty, user, sort, startIndex, limit);
- }
-
- if ((!stubType.HasValue || stubType.Value != StubType.Folder)
- && item is IHasCollectionType collectionFolder)
- {
- if (string.Equals(CollectionType.Music, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+ var collectionType = collectionFolder.CollectionType;
+ if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.Movies, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.TvShows, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetTvFolders(item, user, stubType, sort, startIndex, limit);
}
- else if (string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetFolders(user, startIndex, limit);
}
- else if (string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
{
return GetLiveTvChannels(user, sort, startIndex, limit);
}
}
- if (stubType.HasValue)
+ if (stubType.HasValue && stubType.Value != StubType.Folder)
{
- if (stubType.Value != StubType.Folder)
- {
- return ApplyPaging(new QueryResult<ServerItem>(), startIndex, limit);
- }
+ // TODO should this be doing something?
+ return new QueryResult<ServerItem>();
}
var folder = (Folder)item;
@@ -649,13 +611,12 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsPlaceHolder = false,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, folder.IsPreSorted)
};
- SetSorting(query, sort, folder.IsPreSorted);
-
var queryResult = folder.GetItems(query);
return ToResult(queryResult);
@@ -675,10 +636,9 @@ namespace Emby.Dlna.ContentDirectory
{
StartIndex = startIndex,
Limit = limit,
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
+ OrderBy = GetOrderBy(sort, false)
};
- query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
-
- SetSorting(query, sort, false);
var result = _libraryManager.GetItemsResult(query);
@@ -700,117 +660,57 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.Latest)
- {
- return GetMusicLatest(item, user, query);
+ switch (stubType)
+ {
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Audio);
+ case StubType.Playlists:
+ return GetMusicPlaylists(query);
+ case StubType.Albums:
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
+ case StubType.Artists:
+ return GetMusicArtists(item, query);
+ case StubType.AlbumArtists:
+ return GetMusicAlbumArtists(item, query);
+ case StubType.FavoriteAlbums:
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
+ case StubType.FavoriteArtists:
+ return GetFavoriteArtists(item, query);
+ case StubType.FavoriteSongs:
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
+ case StubType.Songs:
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio);
+ case StubType.Genres:
+ return GetMusicGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.Playlists)
- {
- return GetMusicPlaylists(user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Albums)
- {
- return GetMusicAlbums(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Artists)
- {
- return GetMusicArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.AlbumArtists)
- {
- return GetMusicAlbumArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteAlbums)
- {
- return GetFavoriteAlbums(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteArtists)
- {
- return GetFavoriteArtists(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteSongs)
- {
- return GetFavoriteSongs(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Songs)
- {
- return GetMusicSongs(item, user, query);
- }
+ var serverItems = new ServerItem[]
+ {
+ new (item, StubType.Latest),
+ new (item, StubType.Playlists),
+ new (item, StubType.Albums),
+ new (item, StubType.AlbumArtists),
+ new (item, StubType.Artists),
+ new (item, StubType.Songs),
+ new (item, StubType.Genres),
+ new (item, StubType.FavoriteArtists),
+ new (item, StubType.FavoriteAlbums),
+ new (item, StubType.FavoriteSongs)
+ };
- if (stubType.HasValue && stubType.Value == StubType.Genres)
+ if (limit < serverItems.Length)
{
- return GetMusicGenres(item, user, query);
+ serverItems = serverItems[..limit.Value];
}
- var list = new List<ServerItem>
- {
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Playlists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Albums
- },
-
- new ServerItem(item)
- {
- StubType = StubType.AlbumArtists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Artists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Songs
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Genres
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteArtists
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteAlbums
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteSongs
- }
- };
-
return new QueryResult<ServerItem>
{
- Items = list,
- TotalRecordCount = list.Count
+ Items = serverItems,
+ TotalRecordCount = serverItems.Length
};
}
@@ -829,68 +729,41 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
- if (stubType.HasValue && stubType.Value == StubType.ContinueWatching)
- {
- return GetMovieContinueWatching(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Latest)
- {
- return GetMovieLatest(item, user, query);
+ switch (stubType)
+ {
+ case StubType.ContinueWatching:
+ return GetMovieContinueWatching(item, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Movie);
+ case StubType.Movies:
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie);
+ case StubType.Collections:
+ return GetMovieCollections(query);
+ case StubType.Favorites:
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
+ case StubType.Genres:
+ return GetGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.Movies)
+ var array = new ServerItem[]
{
- return GetMovieMovies(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Collections)
- {
- return GetMovieCollections(user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Favorites)
- {
- return GetMovieFavorites(item, user, query);
- }
+ new (item, StubType.ContinueWatching),
+ new (item, StubType.Latest),
+ new (item, StubType.Movies),
+ new (item, StubType.Collections),
+ new (item, StubType.Favorites),
+ new (item, StubType.Genres)
+ };
- if (stubType.HasValue && stubType.Value == StubType.Genres)
+ if (limit < array.Length)
{
- return GetGenres(item, user, query);
+ array = array[..limit.Value];
}
- var array = new[]
- {
- new ServerItem(item)
- {
- StubType = StubType.ContinueWatching
- },
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
- new ServerItem(item)
- {
- StubType = StubType.Movies
- },
- new ServerItem(item)
- {
- StubType = StubType.Collections
- },
- new ServerItem(item)
- {
- StubType = StubType.Favorites
- },
- new ServerItem(item)
- {
- StubType = StubType.Genres
- }
- };
-
return new QueryResult<ServerItem>
{
Items = array,
@@ -907,22 +780,21 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private QueryResult<ServerItem> GetFolders(User user, int? startIndex, int? limit)
{
- var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true);
+ var totalRecordCount = folders.Count;
+ // Handle paging
+ var items = folders
.OrderBy(i => i.SortName)
- .Select(i => new ServerItem(i)
- {
- StubType = StubType.Folder
- })
+ .Skip(startIndex ?? 0)
+ .Take(limit ?? int.MaxValue)
+ .Select(i => new ServerItem(i, StubType.Folder))
.ToArray();
- return ApplyPaging(
- new QueryResult<ServerItem>
- {
- Items = folders,
- TotalRecordCount = folders.Length
- },
- startIndex,
- limit);
+ return new QueryResult<ServerItem>
+ {
+ Items = items,
+ TotalRecordCount = totalRecordCount
+ };
}
/// <summary>
@@ -940,87 +812,48 @@ namespace Emby.Dlna.ContentDirectory
var query = new InternalItemsQuery(user)
{
StartIndex = startIndex,
- Limit = limit
+ Limit = limit,
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
- if (stubType.HasValue && stubType.Value == StubType.ContinueWatching)
- {
- return GetMovieContinueWatching(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.NextUp)
- {
- return GetNextUp(item, query);
- }
- if (stubType.HasValue && stubType.Value == StubType.Latest)
- {
- return GetTvLatest(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.Series)
- {
- return GetSeries(item, user, query);
- }
-
- if (stubType.HasValue && stubType.Value == StubType.FavoriteSeries)
- {
- return GetFavoriteSeries(item, user, query);
+ switch (stubType)
+ {
+ case StubType.ContinueWatching:
+ return GetMovieContinueWatching(item, query);
+ case StubType.NextUp:
+ return GetNextUp(item, query);
+ case StubType.Latest:
+ return GetLatest(item, query, BaseItemKind.Episode);
+ case StubType.Series:
+ return GetChildrenOfItem(item, query, BaseItemKind.Series);
+ case StubType.FavoriteSeries:
+ return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
+ case StubType.FavoriteEpisodes:
+ return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
+ case StubType.Genres:
+ return GetGenres(item, query);
}
- if (stubType.HasValue && stubType.Value == StubType.FavoriteEpisodes)
+ var serverItems = new ServerItem[]
{
- return GetFavoriteEpisodes(item, user, query);
- }
+ new (item, StubType.ContinueWatching),
+ new (item, StubType.NextUp),
+ new (item, StubType.Latest),
+ new (item, StubType.Series),
+ new (item, StubType.FavoriteSeries),
+ new (item, StubType.FavoriteEpisodes),
+ new (item, StubType.Genres)
+ };
- if (stubType.HasValue && stubType.Value == StubType.Genres)
+ if (limit < serverItems.Length)
{
- return GetGenres(item, user, query);
+ serverItems = serverItems[..limit.Value];
}
- var list = new List<ServerItem>
- {
- new ServerItem(item)
- {
- StubType = StubType.ContinueWatching
- },
-
- new ServerItem(item)
- {
- StubType = StubType.NextUp
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Latest
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Series
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteSeries
- },
-
- new ServerItem(item)
- {
- StubType = StubType.FavoriteEpisodes
- },
-
- new ServerItem(item)
- {
- StubType = StubType.Genres
- }
- };
-
return new QueryResult<ServerItem>
{
- Items = list,
- TotalRecordCount = list.Count
+ Items = serverItems,
+ TotalRecordCount = serverItems.Length
};
}
@@ -1028,14 +861,12 @@ namespace Emby.Dlna.ContentDirectory
/// Returns the Movies that are part watched that meet the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMovieContinueWatching(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetMovieContinueWatching(BaseItem parent, InternalItemsQuery query)
{
query.Recursive = true;
query.Parent = parent;
- query.SetUser(user);
query.OrderBy = new[]
{
@@ -1044,47 +875,7 @@ namespace Emby.Dlna.ContentDirectory
};
query.IsResumable = true;
- query.Limit = 10;
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the series meeting the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetSeries(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(Series) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the Movie folders meeting the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMovieMovies(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.Limit ??= 10;
var result = _libraryManager.GetItemsResult(query);
@@ -1094,36 +885,12 @@ namespace Emby.Dlna.ContentDirectory
/// <summary>
/// Returns the Movie collections meeting the criteria.
/// </summary>
- /// <param name="user">The see cref="User"/>.</param>
/// <param name="query">The see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- // query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(BoxSet) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the Music albums meeting the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicAlbums(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
{
query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
+ query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
var result = _libraryManager.GetItemsResult(query);
@@ -1131,119 +898,19 @@ namespace Emby.Dlna.ContentDirectory
}
/// <summary>
- /// Returns the Music songs meeting the criteria.
+ /// Returns the children that meet the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <param name="itemType">The item type.</param>
+ /// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicSongs(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
{
query.Recursive = true;
query.Parent = parent;
- query.SetUser(user);
-
- query.IncludeItemTypes = new[] { nameof(Audio) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the songs tagged as favourite that meet the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetFavoriteSongs(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Audio) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the series tagged as favourite that meet the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetFavoriteSeries(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Series) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the episodes tagged as favourite that meet the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetFavoriteEpisodes(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Episode) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// Returns the movies tagged as favourite that meet the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMovieFavorites(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Movie) };
-
- var result = _libraryManager.GetItemsResult(query);
-
- return ToResult(result);
- }
-
- /// <summary>
- /// /// Returns the albums tagged as favourite that meet the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetFavoriteAlbums(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.Recursive = true;
- query.Parent = parent;
- query.SetUser(user);
- query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
+ query.IsFavorite = isFavorite;
+ query.IncludeItemTypes = new[] { itemType };
var result = _libraryManager.GetItemsResult(query);
@@ -1255,139 +922,90 @@ namespace Emby.Dlna.ContentDirectory
/// The GetGenres.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetGenres(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetGenres(BaseItem parent, InternalItemsQuery query)
{
- var genresResult = _libraryManager.GetGenres(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
-
- var result = new QueryResult<BaseItem>
- {
- TotalRecordCount = genresResult.TotalRecordCount,
- Items = genresResult.Items.Select(i => i.Item1).ToArray()
- };
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var genresResult = _libraryManager.GetGenres(query);
- return ToResult(result);
+ return ToResult(genresResult);
}
/// <summary>
/// Returns the music genres meeting the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, InternalItemsQuery query)
{
- var genresResult = _libraryManager.GetMusicGenres(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
-
- var result = new QueryResult<BaseItem>
- {
- TotalRecordCount = genresResult.TotalRecordCount,
- Items = genresResult.Items.Select(i => i.Item1).ToArray()
- };
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var genresResult = _libraryManager.GetMusicGenres(query);
- return ToResult(result);
+ return ToResult(genresResult);
}
/// <summary>
/// Returns the music albums by artist that meet the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetAlbumArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
-
- var result = new QueryResult<BaseItem>
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var artists = _libraryManager.GetAlbumArtists(query);
- return ToResult(result);
+ return ToResult(artists);
}
/// <summary>
/// Returns the music artists meeting the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit
- });
-
- var result = new QueryResult<BaseItem>
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ var artists = _libraryManager.GetArtists(query);
+ return ToResult(artists);
}
/// <summary>
/// Returns the artists tagged as favourite that meet the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
{
- var artists = _libraryManager.GetArtists(new InternalItemsQuery(user)
- {
- AncestorIds = new[] { parent.Id },
- StartIndex = query.StartIndex,
- Limit = query.Limit,
- IsFavorite = true
- });
-
- var result = new QueryResult<BaseItem>
- {
- TotalRecordCount = artists.TotalRecordCount,
- Items = artists.Items.Select(i => i.Item1).ToArray()
- };
-
- return ToResult(result);
+ // Don't sort
+ query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.AncestorIds = new[] { parent.Id };
+ query.IsFavorite = true;
+ var artists = _libraryManager.GetArtists(query);
+ return ToResult(artists);
}
/// <summary>
/// Returns the music playlists meeting the criteria.
/// </summary>
- /// <param name="user">The user<see cref="User"/>.</param>
/// <param name="query">The query<see cref="InternalItemsQuery"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { nameof(Playlist) };
- query.SetUser(user);
+ query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
query.Recursive = true;
var result = _libraryManager.GetItemsResult(query);
@@ -1396,31 +1014,6 @@ namespace Emby.Dlna.ContentDirectory
}
/// <summary>
- /// Returns the latest music meeting the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicLatest(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.OrderBy = Array.Empty<(string, SortOrder)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Audio) },
- ParentId = parent?.Id ?? Guid.Empty,
- GroupItems = true
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
-
- return ToResult(items);
- }
-
- /// <summary>
/// Returns the next up item meeting the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
@@ -1435,7 +1028,8 @@ namespace Emby.Dlna.ContentDirectory
{
Limit = query.Limit,
StartIndex = query.StartIndex,
- UserId = query.User.Id
+ // User cannot be null here as the caller has set it
+ UserId = query.User!.Id
},
new[] { parent },
query.DtoOptions);
@@ -1444,47 +1038,23 @@ namespace Emby.Dlna.ContentDirectory
}
/// <summary>
- /// Returns the latest tv meeting the criteria.
+ /// Returns the latest items of [itemType] meeting the criteria.
/// </summary>
/// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
/// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
+ /// <param name="itemType">The item type.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetTvLatest(BaseItem parent, User user, InternalItemsQuery query)
+ private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Episode) },
- ParentId = parent == null ? Guid.Empty : parent.Id,
- GroupItems = false
- },
- query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
-
- return ToResult(items);
- }
-
- /// <summary>
- /// Returns the latest movies meeting the criteria.
- /// </summary>
- /// <param name="parent">The <see cref="BaseItem"/>.</param>
- /// <param name="user">The <see cref="User"/>.</param>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMovieLatest(BaseItem parent, User user, InternalItemsQuery query)
- {
- query.OrderBy = Array.Empty<(string, SortOrder)>();
-
- var items = _userViewManager.GetLatestItems(
- new LatestItemsQuery
- {
- UserId = user.Id,
- Limit = 50,
- IncludeItemTypes = new[] { nameof(Movie) },
+ // User cannot be null here as the caller has set it
+ UserId = query.User!.Id,
+ Limit = query.Limit ?? 50,
+ IncludeItemTypes = new[] { itemType },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
},
@@ -1497,27 +1067,24 @@ namespace Emby.Dlna.ContentDirectory
/// Returns music artist items that meet the criteria.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
- /// <param name="parentId">The <see cref="Guid"/>.</param>
/// <param name="user">The <see cref="User"/>.</param>
/// <param name="sort">The <see cref="SortCriteria"/>.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="limit">The maximum number to return.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicArtistItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
+ private QueryResult<ServerItem> GetMusicArtistItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
@@ -1527,31 +1094,28 @@ namespace Emby.Dlna.ContentDirectory
/// Returns the genre items meeting the criteria.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
- /// <param name="parentId">The <see cref="Guid"/>.</param>
/// <param name="user">The <see cref="User"/>.</param>
/// <param name="sort">The <see cref="SortCriteria"/>.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="limit">The maximum number to return.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
+ private QueryResult<ServerItem> GetGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
GenreIds = new[] { item.Id },
IncludeItemTypes = new[]
{
- nameof(Movie),
- nameof(Series)
+ BaseItemKind.Movie,
+ BaseItemKind.Series
},
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
@@ -1561,46 +1125,43 @@ namespace Emby.Dlna.ContentDirectory
/// Returns the music genre items meeting the criteria.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
- /// <param name="parentId">The <see cref="Guid"/>.</param>
/// <param name="user">The <see cref="User"/>.</param>
/// <param name="sort">The <see cref="SortCriteria"/>.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="limit">The maximum number to return.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private QueryResult<ServerItem> GetMusicGenreItems(BaseItem item, Guid parentId, User user, SortCriteria sort, int? startIndex, int? limit)
+ private QueryResult<ServerItem> GetMusicGenreItems(BaseItem item, User user, SortCriteria sort, int? startIndex, int? limit)
{
var query = new InternalItemsQuery(user)
{
Recursive = true,
- ParentId = parentId,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
- DtoOptions = GetDtoOptions()
+ DtoOptions = GetDtoOptions(),
+ OrderBy = GetOrderBy(sort, false)
};
- SetSorting(query, sort, false);
-
var result = _libraryManager.GetItemsResult(query);
return ToResult(result);
}
/// <summary>
- /// Converts a <see cref="BaseItem"/> array into a <see cref="QueryResult{ServerItem}"/>.
+ /// Converts <see cref="IReadOnlyCollection{BaseItem}"/> into a <see cref="QueryResult{ServerItem}"/>.
/// </summary>
/// <param name="result">An array of <see cref="BaseItem"/>.</param>
/// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns>
- private static QueryResult<ServerItem> ToResult(BaseItem[] result)
+ private static QueryResult<ServerItem> ToResult(IReadOnlyCollection<BaseItem> result)
{
var serverItems = result
- .Select(i => new ServerItem(i))
+ .Select(i => new ServerItem(i, null))
.ToArray();
return new QueryResult<ServerItem>
{
- TotalRecordCount = result.Length,
+ TotalRecordCount = result.Count,
Items = serverItems
};
}
@@ -1612,10 +1173,12 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
private static QueryResult<ServerItem> ToResult(QueryResult<BaseItem> result)
{
- var serverItems = result
- .Items
- .Select(i => new ServerItem(i))
- .ToArray();
+ var length = result.Items.Count;
+ var serverItems = new ServerItem[length];
+ for (var i = 0; i < length; i++)
+ {
+ serverItems[i] = new ServerItem(result.Items[i], null);
+ }
return new QueryResult<ServerItem>
{
@@ -1625,35 +1188,34 @@ namespace Emby.Dlna.ContentDirectory
}
/// <summary>
- /// Sets the sorting method on a query.
+ /// Converts a query result to a <see cref="QueryResult{ServerItem}"/>.
/// </summary>
- /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
- /// <param name="sort">The <see cref="SortCriteria"/>.</param>
- /// <param name="isPreSorted">True if pre-sorted.</param>
- private static void SetSorting(InternalItemsQuery query, SortCriteria sort, bool isPreSorted)
+ /// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param>
+ /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
+ private static QueryResult<ServerItem> ToResult(QueryResult<(BaseItem, ItemCounts)> result)
{
- if (isPreSorted)
+ var length = result.Items.Count;
+ var serverItems = new ServerItem[length];
+ for (var i = 0; i < length; i++)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ serverItems[i] = new ServerItem(result.Items[i].Item1, null);
}
- else
+
+ return new QueryResult<ServerItem>
{
- query.OrderBy = new[] { (ItemSortBy.SortName, sort.SortOrder) };
- }
+ TotalRecordCount = result.TotalRecordCount,
+ Items = serverItems
+ };
}
/// <summary>
- /// Apply paging to a query.
+ /// Gets the sorting method on a query.
/// </summary>
- /// <param name="result">The <see cref="QueryResult{ServerItem}"/>.</param>
- /// <param name="startIndex">The start index.</param>
- /// <param name="limit">The maximum number to return.</param>
- /// <returns>A <see cref="QueryResult{ServerItem}"/>.</returns>
- private static QueryResult<ServerItem> ApplyPaging(QueryResult<ServerItem> result, int? startIndex, int? limit)
+ /// <param name="sort">The <see cref="SortCriteria"/>.</param>
+ /// <param name="isPreSorted">True if pre-sorted.</param>
+ private static (string, SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
- result.Items = result.Items.Skip(startIndex ?? 0).Take(limit ?? int.MaxValue).ToArray();
-
- return result;
+ return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
/// <summary>
@@ -1664,7 +1226,7 @@ namespace Emby.Dlna.ContentDirectory
private ServerItem GetItemFromObjectId(string id)
{
return DidlBuilder.IsIdRoot(id)
- ? new ServerItem(_libraryManager.GetUserRootFolder())
+ ? new ServerItem(_libraryManager.GetUserRootFolder(), null)
: ParseItemId(id);
}
@@ -1682,37 +1244,29 @@ namespace Emby.Dlna.ContentDirectory
var paramsIndex = id.IndexOf(ParamsSrch, StringComparison.OrdinalIgnoreCase);
if (paramsIndex != -1)
{
- id = id.Substring(paramsIndex + ParamsSrch.Length);
+ id = id[(paramsIndex + ParamsSrch.Length)..];
var parts = id.Split(';');
id = parts[23];
}
- var enumNames = Enum.GetNames(typeof(StubType));
- foreach (var name in enumNames)
+ var dividerIndex = id.IndexOf('_', StringComparison.Ordinal);
+ if (dividerIndex != -1 && Enum.TryParse<StubType>(id.AsSpan(0, dividerIndex), true, out var parsedStubType))
{
- if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
- {
- stubType = Enum.Parse<StubType>(name, true);
- id = id.Split('_', 2)[1];
-
- break;
- }
+ id = id[(dividerIndex + 1)..];
+ stubType = parsedStubType;
}
if (Guid.TryParse(id, out var itemId))
{
var item = _libraryManager.GetItemById(itemId);
- return new ServerItem(item)
- {
- StubType = stubType
- };
+ return new ServerItem(item, stubType);
}
Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id);
- return new ServerItem(_libraryManager.GetUserRootFolder());
+ return new ServerItem(_libraryManager.GetUserRootFolder(), null);
}
}
}
diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs
index 34244000c..df05fa966 100644
--- a/Emby.Dlna/ContentDirectory/ServerItem.cs
+++ b/Emby.Dlna/ContentDirectory/ServerItem.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory
@@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
/// Initializes a new instance of the <see cref="ServerItem"/> class.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
- public ServerItem(BaseItem item)
+ /// <param name="stubType">The stub type.</param>
+ public ServerItem(BaseItem item, StubType? stubType)
{
Item = item;
- if (item is IItemByName && !(item is Folder))
+ if (stubType.HasValue)
+ {
+ StubType = stubType;
+ }
+ else if (item is IItemByName and not Folder)
{
StubType = Dlna.ContentDirectory.StubType.Folder;
}
}
/// <summary>
- /// Gets or sets the underlying base item.
+ /// Gets the underlying base item.
/// </summary>
- public BaseItem Item { get; set; }
+ public BaseItem Item { get; }
/// <summary>
- /// Gets or sets the DLNA item type.
+ /// Gets the DLNA item type.
/// </summary>
- public StubType? StubType { get; set; }
+ public StubType? StubType { get; }
}
}
diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs
index a7f2d4a73..8b0958842 100644
--- a/Emby.Dlna/ControlResponse.cs
+++ b/Emby.Dlna/ControlResponse.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -8,9 +6,11 @@ namespace Emby.Dlna
{
public class ControlResponse
{
- public ControlResponse()
+ public ControlResponse(string xml, bool isSuccessful)
{
Headers = new Dictionary<string, string>();
+ Xml = xml;
+ IsSuccessful = isSuccessful;
}
public IDictionary<string, string> Headers { get; }
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 2982ce97e..b00e1c98a 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -41,8 +41,6 @@ namespace Emby.Dlna.Didl
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
@@ -317,7 +315,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -328,7 +326,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -342,7 +340,7 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (filter.Contains("res@resolution"))
@@ -361,12 +359,12 @@ namespace Emby.Dlna.Didl
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (totalBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetVideoMediaProfile(
@@ -552,7 +550,7 @@ namespace Emby.Dlna.Didl
if (mediaSource.RunTimeTicks.HasValue)
{
- writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture));
+ writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
if (filter.Contains("res@size"))
@@ -563,7 +561,7 @@ namespace Emby.Dlna.Didl
if (size.HasValue)
{
- writer.WriteAttributeString("size", size.Value.ToString(_usCulture));
+ writer.WriteAttributeString("size", size.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
@@ -575,17 +573,17 @@ namespace Emby.Dlna.Didl
if (targetChannels.HasValue)
{
- writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture));
+ writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetSampleRate.HasValue)
{
- writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
if (targetAudioBitrate.HasValue)
{
- writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
+ writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
var mediaProfile = _profile.GetAudioMediaProfile(
@@ -639,7 +637,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("restricted", "1");
writer.WriteAttributeString("searchable", "1");
- writer.WriteAttributeString("childCount", childCount.ToString(_usCulture));
+ writer.WriteAttributeString("childCount", childCount.ToString(CultureInfo.InvariantCulture));
var clientId = GetClientId(folder, stubType);
@@ -731,7 +729,7 @@ namespace Emby.Dlna.Didl
{
if (item.PremiereDate.HasValue)
{
- AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
+ AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
}
}
@@ -748,7 +746,7 @@ namespace Emby.Dlna.Didl
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
}
- if (!(item is Folder))
+ if (item is not Folder)
{
if (filter.Contains("dc:description"))
{
@@ -931,11 +929,11 @@ namespace Emby.Dlna.Didl
if (item.IndexNumber.HasValue)
{
- AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
if (item is Episode)
{
- AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
+ AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), NsUpnp);
}
}
}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index a1b106704..d9d2a345a 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -1,22 +1,18 @@
-#nullable disable
-
#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -96,12 +92,14 @@ namespace Emby.Dlna
}
}
+ /// <inheritdoc />
public DeviceProfile GetDefaultProfile()
{
return new DefaultProfile();
}
- public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
+ /// <inheritdoc />
+ public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
{
if (deviceInfo == null)
{
@@ -111,35 +109,18 @@ namespace Emby.Dlna
var profile = GetProfiles()
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
- if (profile != null)
+ if (profile == null)
{
- _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
+ _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
}
else
{
- LogUnmatchedProfile(deviceInfo);
+ _logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
return profile;
}
- private void LogUnmatchedProfile(DeviceIdentification profile)
- {
- var builder = new StringBuilder();
-
- builder.AppendLine("No matching device profile found. The default will need to be used.");
- builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
- builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
- builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
- builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
- builder.Append("ModelName: ").AppendLine(profile.ModelName);
- builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
- builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
- builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
-
- _logger.LogInformation(builder.ToString());
- }
-
/// <summary>
/// Attempts to match a device with a profile.
/// Rules:
@@ -187,7 +168,8 @@ namespace Emby.Dlna
}
}
- public DeviceProfile GetProfile(IHeaderDictionary headers)
+ /// <inheritdoc />
+ public DeviceProfile? GetProfile(IHeaderDictionary headers)
{
if (headers == null)
{
@@ -195,15 +177,13 @@ namespace Emby.Dlna
}
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
-
- if (profile != null)
+ if (profile == null)
{
- _logger.LogDebug("Found matching device profile: {0}", profile.Name);
+ _logger.LogDebug("No matching device profile found. {@Headers}", headers);
}
else
{
- var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
- _logger.LogDebug("No matching device profile found. {0}", headerString);
+ _logger.LogDebug("Found matching device profile: {0}", profile.Name);
}
return profile;
@@ -253,19 +233,19 @@ namespace Emby.Dlna
return xmlFies
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
- .ToList();
+ .ToList()!; // We just filtered out all the nulls
}
catch (IOException)
{
- return new List<DeviceProfile>();
+ return Array.Empty<DeviceProfile>();
}
}
- private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
+ private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
{
lock (_profiles)
{
- if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
+ if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
{
return profileTuple.Item2;
}
@@ -293,7 +273,8 @@ namespace Emby.Dlna
}
}
- public DeviceProfile GetProfile(string id)
+ /// <inheritdoc />
+ public DeviceProfile? GetProfile(string id)
{
if (string.IsNullOrEmpty(id))
{
@@ -322,6 +303,7 @@ namespace Emby.Dlna
}
}
+ /// <inheritdoc />
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
{
return GetProfileInfosInternal().Select(i => i.Info);
@@ -329,17 +311,14 @@ namespace Emby.Dlna
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
{
- return new InternalProfileInfo
- {
- Path = file.FullName,
-
- Info = new DeviceProfileInfo
+ return new InternalProfileInfo(
+ new DeviceProfileInfo
{
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type
- }
- };
+ },
+ file.FullName);
}
private async Task ExtractSystemProfilesAsync()
@@ -359,16 +338,20 @@ namespace Emby.Dlna
systemProfilesPath,
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
- using (var stream = _assembly.GetManifestResourceStream(name))
+ // The stream should exist as we just got its name from GetManifestResourceNames
+ using (var stream = _assembly.GetManifestResourceStream(name)!)
{
+ var length = stream.Length;
var fileInfo = _fileSystem.GetFileInfo(path);
- if (!fileInfo.Exists || fileInfo.Length != stream.Length)
+ if (!fileInfo.Exists || fileInfo.Length != length)
{
Directory.CreateDirectory(systemProfilesPath);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.Create;
+ fileOptions.PreallocationSize = length;
+ using (var fileStream = new FileStream(path, fileOptions))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
@@ -380,6 +363,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(UserProfilesPath);
}
+ /// <inheritdoc />
public void DeleteProfile(string id)
{
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@@ -397,6 +381,7 @@ namespace Emby.Dlna
}
}
+ /// <inheritdoc />
public void CreateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -412,7 +397,8 @@ namespace Emby.Dlna
SaveProfile(profile, path, DeviceProfileType.User);
}
- public void UpdateProfile(DeviceProfile profile)
+ /// <inheritdoc />
+ public void UpdateProfile(string profileId, DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@@ -426,7 +412,7 @@ namespace Emby.Dlna
throw new ArgumentException("Profile is missing Name");
}
- var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
+ var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename);
@@ -470,9 +456,11 @@ namespace Emby.Dlna
var json = JsonSerializer.Serialize(profile, _jsonOptions);
- return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
+ // Output can't be null if the input isn't null
+ return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
}
+ /// <inheritdoc />
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetDefaultProfile();
@@ -482,26 +470,37 @@ namespace Emby.Dlna
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
}
- public ImageStream GetIcon(string filename)
+ /// <inheritdoc />
+ public ImageStream? GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
? ImageFormat.Png
: ImageFormat.Jpg;
var resource = GetType().Namespace + ".Images." + filename.ToLowerInvariant();
+ var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream == null)
+ {
+ return null;
+ }
- return new ImageStream
+ return new ImageStream(stream)
{
- Format = format,
- Stream = _assembly.GetManifestResourceStream(resource)
+ Format = format
};
}
private class InternalProfileInfo
{
- internal DeviceProfileInfo Info { get; set; }
+ internal InternalProfileInfo(DeviceProfileInfo info, string path)
+ {
+ Info = info;
+ Path = path;
+ }
+
+ internal DeviceProfileInfo Info { get; }
- internal string Path { get; set; }
+ internal string Path { get; }
}
}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index a40578e40..7fdbd44f0 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -17,11 +17,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
</PropertyGroup>
<!-- Code Analyzers-->
@@ -31,10 +29,6 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<EmbeddedResource Include="Images\logo120.jpg" />
<EmbeddedResource Include="Images\logo120.png" />
@@ -78,7 +72,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
</ItemGroup>
</Project>
diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs
index 8c82dcbf6..635d2c47a 100644
--- a/Emby.Dlna/EventSubscriptionResponse.cs
+++ b/Emby.Dlna/EventSubscriptionResponse.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -8,8 +6,10 @@ namespace Emby.Dlna
{
public class EventSubscriptionResponse
{
- public EventSubscriptionResponse()
+ public EventSubscriptionResponse(string content, string contentType)
{
+ Content = content;
+ ContentType = contentType;
Headers = new Dictionary<string, string>();
}
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index 2e672b886..d17e23871 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
@@ -25,8 +26,6 @@ namespace Emby.Dlna.Eventing
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
public DlnaEventManager(ILogger logger, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
@@ -51,11 +50,7 @@ namespace Emby.Dlna.Eventing
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
}
- return new EventSubscriptionResponse
- {
- Content = string.Empty,
- ContentType = "text/plain"
- };
+ return new EventSubscriptionResponse(string.Empty, "text/plain");
}
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@@ -86,9 +81,7 @@ namespace Emby.Dlna.Eventing
if (!string.IsNullOrEmpty(header))
{
// Starts with SECOND-
- header = header.Split('-')[^1];
-
- if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(header.AsSpan().RightPart('-'), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return val;
}
@@ -103,23 +96,15 @@ namespace Emby.Dlna.Eventing
_subscriptions.TryRemove(subscriptionId, out _);
- return new EventSubscriptionResponse
- {
- Content = string.Empty,
- ContentType = "text/plain"
- };
+ return new EventSubscriptionResponse(string.Empty, "text/plain");
}
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{
- var response = new EventSubscriptionResponse
- {
- Content = string.Empty,
- ContentType = "text/plain"
- };
+ var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
- response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
+ response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(CultureInfo.InvariantCulture)) : requestedTimeoutString;
return response;
}
@@ -176,7 +161,7 @@ namespace Emby.Dlna.Eventing
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
options.Headers.TryAddWithoutValidation("SID", subscription.Id);
- options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("SEQ", subscription.TriggerCount.ToString(CultureInfo.InvariantCulture));
try
{
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 0309926ab..08f639d93 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -27,11 +27,9 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main
{
@@ -54,7 +52,6 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
- private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled;
private PlayToManager _manager;
@@ -127,8 +124,8 @@ namespace Emby.Dlna.Main
config);
Current = this;
- _netConfig = config.GetConfiguration<NetworkConfiguration>("network");
- _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
+ var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
+ _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
@@ -204,8 +201,8 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer == null)
{
- var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
- OperatingSystem.Id == OperatingSystemId.Linux;
+ var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
+ OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
@@ -221,11 +218,6 @@ namespace Emby.Dlna.Main
}
}
- private void LogMessage(string msg)
- {
- _logger.LogDebug(msg);
- }
-
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{
try
@@ -268,9 +260,13 @@ namespace Emby.Dlna.Main
try
{
- _publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+ _publisher = new SsdpDevicePublisher(
+ _communicationsServer,
+ MediaBrowser.Common.System.OperatingSystem.Name,
+ Environment.OSVersion.VersionString,
+ _config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
- LogFunction = LogMessage,
+ LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false
};
@@ -315,15 +311,9 @@ namespace Emby.Dlna.Main
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
- _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
+ _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
- var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
- if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
- {
- // DLNA will only work over http, so we must reset to http:// : {port}.
- uri.Scheme = "http";
- uri.Port = _netConfig.HttpServerPortNumber;
- }
+ var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
var device = new SsdpRootDevice
{
@@ -409,7 +399,6 @@ namespace Emby.Dlna.Main
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
- _config,
_userDataManager,
_localization,
_mediaSourceManager,
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 6c580d15b..0b2288000 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
{
public class Device : IDisposable
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
@@ -640,7 +638,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- Volume = int.Parse(volumeValue, UsCulture);
+ Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture);
if (Volume > 0)
{
@@ -842,7 +840,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(duration)
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Duration = TimeSpan.Parse(duration, UsCulture);
+ Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture);
}
else
{
@@ -854,7 +852,7 @@ namespace Emby.Dlna.PlayTo
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
- Position = TimeSpan.Parse(position, UsCulture);
+ Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture);
}
var track = result.Document.Descendants("TrackMetaData").FirstOrDefault();
@@ -1194,8 +1192,8 @@ namespace Emby.Dlna.PlayTo
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
- var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
- var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
+ var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
return new DeviceIcon
{
@@ -1260,10 +1258,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
}
private void OnPlaybackProgress(UBaseObject mediaInfo)
@@ -1273,27 +1268,17 @@ namespace Emby.Dlna.PlayTo
return;
}
- PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
}
private void OnPlaybackStop(UBaseObject mediaInfo)
{
- PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
- {
- MediaInfo = mediaInfo
- });
+ PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
}
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{
- MediaChanged?.Invoke(this, new MediaChangedEventArgs
- {
- OldMediaInfo = old,
- NewMediaInfo = newMedia
- });
+ MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
}
/// <inheritdoc />
diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
index 2bc4d8cc2..0f7a524d6 100644
--- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
@@ -1,6 +1,4 @@
-#nullable disable
-
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using System;
@@ -8,6 +6,12 @@ namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
+ public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
+ {
+ OldMediaInfo = oldMediaInfo;
+ NewMediaInfo = newMediaInfo;
+ }
+
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 0e49fd2c0..f25d8017e 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -30,8 +30,6 @@ namespace Emby.Dlna.PlayTo
{
public class PlayToController : ISessionController, IDisposable
{
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
@@ -716,7 +714,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetAudioStreamIndex:
if (command.Arguments.TryGetValue("Index", out string index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetAudioStreamIndex(val);
}
@@ -728,7 +726,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
- if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
+ if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetSubtitleStreamIndex(val);
}
@@ -740,7 +738,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.SetVolume:
if (command.Arguments.TryGetValue("Volume", out string vol))
{
- if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
+ if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 35bf5927c..294bda5b6 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
- public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+ public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
_logger = logger;
_sessionManager = sessionManager;
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
_imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery;
_httpClientFactory = httpClientFactory;
- _config = config;
_userDataManager = userDataManager;
_localization = localization;
_mediaSourceManager = mediaSourceManager;
@@ -173,7 +170,9 @@ namespace Emby.Dlna.PlayTo
uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null);
+ var sessionInfo = await _sessionManager
+ .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null)
+ .ConfigureAwait(false);
var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault();
diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
index c7d2b28df..c95d8b1e8 100644
--- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackProgressEventArgs : EventArgs
{
+ public PlaybackProgressEventArgs(UBaseObject mediaInfo)
+ {
+ MediaInfo = mediaInfo;
+ }
+
public UBaseObject MediaInfo { get; set; }
}
}
diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
index f8a14f411..619c861ed 100644
--- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStartEventArgs : EventArgs
{
+ public PlaybackStartEventArgs(UBaseObject mediaInfo)
+ {
+ MediaInfo = mediaInfo;
+ }
+
public UBaseObject MediaInfo { get; set; }
}
}
diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
index 6661f92ac..d0ec25059 100644
--- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
+++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStoppedEventArgs : EventArgs
{
+ public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
+ {
+ MediaInfo = mediaInfo;
+ }
+
public UBaseObject MediaInfo { get; set; }
}
}
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
index f14f73bb6..cade7b4c2 100644
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
@@ -20,8 +20,6 @@ namespace Emby.Dlna.PlayTo
private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
private const string FriendlyName = "Jellyfin";
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly IHttpClientFactory _httpClientFactory;
public SsdpHttpClient(IHttpClientFactory httpClientFactory)
@@ -45,10 +43,12 @@ namespace Emby.Dlna.PlayTo
header,
cancellationToken)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
@@ -78,14 +78,15 @@ namespace Emby.Dlna.PlayTo
{
using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(_usCulture));
- options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(_usCulture) + ">");
+ options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
+ options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
options.Headers.TryAddWithoutValidation("NT", "upnp:event");
- options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(_usCulture));
+ options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
}
public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
@@ -94,12 +95,13 @@ namespace Emby.Dlna.PlayTo
options.Headers.UserAgent.ParseAdd(USERAGENT);
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
stream,
- LoadOptions.PreserveWhitespace,
+ LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 3f3dfccd3..80a45f2b2 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -15,7 +15,6 @@ namespace Emby.Dlna.Server
{
private readonly DeviceProfile _profile;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly string _serverUdn;
private readonly string _serverAddress;
private readonly string _serverName;
@@ -193,10 +192,10 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
.Append("</mimetype>");
builder.Append("<width>")
- .Append(SecurityElement.Escape(icon.Width.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
.Append("</width>");
builder.Append("<height>")
- .Append(SecurityElement.Escape(icon.Height.ToString(_usCulture)))
+ .Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
.Append("</height>");
builder.Append("<depth>")
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
@@ -250,8 +249,7 @@ namespace Emby.Dlna.Server
url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
- // TODO: @bond remove null-coalescing operator when https://github.com/dotnet/runtime/pull/52442 is merged/released
- return SecurityElement.Escape(url) ?? string.Empty;
+ return SecurityElement.Escape(url);
}
private IEnumerable<DeviceIcon> GetIcons()
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 904c23d99..780aad9c1 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -6,9 +6,9 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
+using Diacritics.Extensions;
using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service
@@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
- Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
+ Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
var settings = new XmlWriterSettings
{
@@ -95,11 +95,7 @@ namespace Emby.Dlna.Service
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
- var controlResponse = new ControlResponse
- {
- Xml = xml,
- IsSuccessful = true
- };
+ var controlResponse = new ControlResponse(xml, true);
controlResponse.Headers.Add("EXT", string.Empty);
diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs
index a97c4d63a..68fd98758 100644
--- a/Emby.Dlna/Service/BaseService.cs
+++ b/Emby.Dlna/Service/BaseService.cs
@@ -23,14 +23,14 @@ namespace Emby.Dlna.Service
return EventManager.CancelEventSubscription(subscriptionId);
}
- public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callbackUrl);
+ return EventManager.RenewEventSubscription(subscriptionId, notificationType, requestedTimeoutString, callbackUrl);
}
- public EventSubscriptionResponse CreateEventSubscription(string notificationType, string timeoutString, string callbackUrl)
+ public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
{
- return EventManager.CreateEventSubscription(notificationType, timeoutString, callbackUrl);
+ return EventManager.CreateEventSubscription(notificationType, requestedTimeoutString, callbackUrl);
}
}
}
diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs
index f2b5dd9ca..3e2cd6d2e 100644
--- a/Emby.Dlna/Service/ControlErrorHandler.cs
+++ b/Emby.Dlna/Service/ControlErrorHandler.cs
@@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
writer.WriteEndDocument();
}
- return new ControlResponse
- {
- Xml = builder.ToString(),
- IsSuccessful = false
- };
+ return new ControlResponse(builder.ToString(), false);
}
}
}
diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj
index 5c5afe1c6..149e4b5d9 100644
--- a/Emby.Drawing/Emby.Drawing.csproj
+++ b/Emby.Drawing/Emby.Drawing.csproj
@@ -6,11 +6,9 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -30,8 +28,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index 7d952aa23..3f75e4fc7 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -26,7 +26,7 @@ namespace Emby.Drawing
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
- private const string Version = "3";
+ private const char Version = '3';
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
@@ -102,7 +102,7 @@ namespace Emby.Drawing
{
var file = await ProcessImage(options).ConfigureAwait(false);
- using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true))
+ using (var fileStream = AsyncFile.OpenRead(file.Item1))
{
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
diff --git a/Emby.Naming/Audio/AudioFileParser.cs b/Emby.Naming/Audio/AudioFileParser.cs
index af4aa0059..2b610ec79 100644
--- a/Emby.Naming/Audio/AudioFileParser.cs
+++ b/Emby.Naming/Audio/AudioFileParser.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
using Emby.Naming.Common;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
diff --git a/Emby.Naming/AudioBook/AudioBookInfo.cs b/Emby.Naming/AudioBook/AudioBookInfo.cs
index 15702ff2c..acd8905af 100644
--- a/Emby.Naming/AudioBook/AudioBookInfo.cs
+++ b/Emby.Naming/AudioBook/AudioBookInfo.cs
@@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
/// <param name="files">List of files composing the actual audiobook.</param>
/// <param name="extras">List of extra files.</param>
/// <param name="alternateVersions">Alternative version of files.</param>
- public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
+ public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
{
Name = name;
Year = year;
@@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
- public List<AudioBookFileInfo> Files { get; set; }
+ public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
- public List<AudioBookFileInfo> Extras { get; set; }
+ public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
- public List<AudioBookFileInfo> AlternateVersions { get; set; }
+ public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
}
}
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index ca5322890..dd8a05bb3 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
public class AudioBookListResolver
{
private readonly NamingOptions _options;
+ private readonly AudioBookResolver _audioBookResolver;
/// <summary>
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
public AudioBookListResolver(NamingOptions options)
{
_options = options;
+ _audioBookResolver = new AudioBookResolver(_options);
}
/// <summary>
@@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{
- var audioBookResolver = new AudioBookResolver(_options);
// File with empty fullname will be sorted out here.
var audiobookFileInfos = files
- .Select(i => audioBookResolver.Resolve(i.FullName))
+ .Select(i => _audioBookResolver.Resolve(i.FullName))
.OfType<AudioBookFileInfo>()
.ToList();
- var stackResult = new StackResolver(_options)
- .ResolveAudioBooks(audiobookFileInfos);
+ var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
foreach (var stack in stackResult)
{
var stackFiles = stack.Files
- .Select(i => audioBookResolver.Resolve(i))
+ .Select(i => _audioBookResolver.Resolve(i))
.OfType<AudioBookFileInfo>()
.ToList();
@@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
foreach (var audioFile in group)
{
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
- if (name.Equals("audiobook") ||
+ if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index f6ad3601d..183b6c3b1 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.AudioBook
{
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 22a3e8bb4..aa62a47f1 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CA1819
+
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
@@ -122,11 +125,11 @@ namespace Emby.Naming.Common
token: "DSR")
};
- VideoFileStackingExpressions = new[]
+ VideoFileStackingRules = new[]
{
- "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
- "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
- "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+ new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -137,8 +140,11 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
- @"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
- @"(\[.*\])"
+ @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
+ @"^(?<cleaned>.+?)(\[.*\])",
+ @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
+ @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
+ @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
};
SubtitleFileExtensions = new[]
@@ -250,6 +256,8 @@ namespace Emby.Naming.Common
},
// <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
+ // <!-- foo.E01., foo.e01. -->
+ new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{
DateTimeFormats = new[]
@@ -277,14 +285,14 @@ namespace Emby.Naming.Common
IsNamed = true
},
- new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
+ new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
- new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
+ new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
@@ -305,6 +313,12 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
+ // "Episode 16", "Episode 16 - Title"
+ new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
+ {
+ IsNamed = true
+ },
+
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{
IsNamed = true
@@ -362,12 +376,20 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
- // "Episode 16", "Episode 16 - Title"
- new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
+
+ // Series and season only expression
+ // "the show/season 1", "the show/s01"
+ new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
+ {
+ IsNamed = true
+ },
+
+ // Series and season only expression
+ // "the show S01", "the show season 1"
+ new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
{
- IsOptimistic = true,
IsNamed = true
- }
+ },
};
EpisodeWithoutSeasonExpressions = new[]
@@ -384,6 +406,12 @@ namespace Emby.Naming.Common
{
new ExtraRule(
ExtraType.Trailer,
+ ExtraRuleType.DirectoryName,
+ "trailers",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Trailer,
ExtraRuleType.Filename,
"trailer",
MediaType.Video),
@@ -449,6 +477,12 @@ namespace Emby.Naming.Common
MediaType.Audio),
new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.DirectoryName,
+ "theme-music",
+ MediaType.Audio),
+
+ new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
"-scene",
@@ -479,6 +513,12 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.Suffix,
+ "-deletedscene",
+ MediaType.Video),
+
+ new ExtraRule(
ExtraType.Clip,
ExtraRuleType.Suffix,
"-featurette",
@@ -536,7 +576,7 @@ namespace Emby.Naming.Common
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
- MediaType.Video),
+ MediaType.Video)
};
Format3DRules = new[]
@@ -648,10 +688,30 @@ namespace Emby.Naming.Common
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
+ AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
+ {
+ ["trailers"] = ExtraType.Trailer,
+ ["theme-music"] = ExtraType.ThemeSong,
+ ["backdrops"] = ExtraType.ThemeVideo,
+ ["extras"] = ExtraType.Unknown,
+ ["behind the scenes"] = ExtraType.BehindTheScenes,
+ ["deleted scenes"] = ExtraType.DeletedScene,
+ ["interviews"] = ExtraType.Interview,
+ ["scenes"] = ExtraType.Scene,
+ ["samples"] = ExtraType.Sample,
+ ["shorts"] = ExtraType.Clip,
+ ["featurettes"] = ExtraType.Clip
+ };
+
Compile();
}
/// <summary>
+ /// Gets or sets the folder name to extra types mapping.
+ /// </summary>
+ public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
+
+ /// <summary>
/// Gets or sets list of audio file extensions.
/// </summary>
public string[] AudioFileExtensions { get; set; }
@@ -732,9 +792,9 @@ namespace Emby.Naming.Common
public Format3DRule[] Format3DRules { get; set; }
/// <summary>
- /// Gets or sets list of raw video file-stacking expressions strings.
+ /// Gets the file stacking rules.
/// </summary>
- public string[] VideoFileStackingExpressions { get; set; }
+ public FileStackRule[] VideoFileStackingRules { get; }
/// <summary>
/// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -757,11 +817,6 @@ namespace Emby.Naming.Common
public ExtraRule[] VideoExtraRules { get; set; }
/// <summary>
- /// Gets list of video file-stack regular expressions.
- /// </summary>
- public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
-
- /// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
@@ -786,7 +841,6 @@ namespace Emby.Naming.Common
/// </summary>
public void Compile()
{
- VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 3224ff412..2bf8eacb1 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.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>
@@ -6,15 +6,13 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
- <Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -40,7 +38,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
@@ -50,8 +48,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
</Project>
diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs
index a19340ef6..5809c512a 100644
--- a/Emby.Naming/Subtitles/SubtitleParser.cs
+++ b/Emby.Naming/Subtitles/SubtitleParser.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Subtitles
{
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
}
var extension = Path.GetExtension(path);
- if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return null;
}
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
var flags = GetFlags(path);
var info = new SubtitleInfo(
path,
- _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
- _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
+ _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
+ _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
- var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
- && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
+ var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
+ && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
.ToList();
// Should have a name, language and file extension
diff --git a/Emby.Naming/TV/EpisodeResolver.cs b/Emby.Naming/TV/EpisodeResolver.cs
index 5e952e47b..6cebc40c2 100644
--- a/Emby.Naming/TV/EpisodeResolver.cs
+++ b/Emby.Naming/TV/EpisodeResolver.cs
@@ -1,8 +1,8 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Extensions;
namespace Emby.Naming.TV
{
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
{
var extension = Path.GetExtension(path);
// Check supported extensions
- if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))
diff --git a/Emby.Naming/TV/SeriesInfo.cs b/Emby.Naming/TV/SeriesInfo.cs
new file mode 100644
index 000000000..5d6cb4bd3
--- /dev/null
+++ b/Emby.Naming/TV/SeriesInfo.cs
@@ -0,0 +1,29 @@
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Holder object for Series information.
+ /// </summary>
+ public class SeriesInfo
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesInfo"/> class.
+ /// </summary>
+ /// <param name="path">Path to the file.</param>
+ public SeriesInfo(string path)
+ {
+ Path = path;
+ }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ public string? Name { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParser.cs b/Emby.Naming/TV/SeriesPathParser.cs
new file mode 100644
index 000000000..4dfbb36a3
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -0,0 +1,60 @@
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Used to parse information about series from paths containing more information that only the series name.
+ /// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
+ /// </summary>
+ public static class SeriesPathParser
+ {
+ /// <summary>
+ /// Parses information about series from path.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
+ /// <param name="path">Path.</param>
+ /// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
+ public static SeriesPathParserResult Parse(NamingOptions options, string path)
+ {
+ SeriesPathParserResult? result = null;
+
+ foreach (var expression in options.EpisodeExpressions)
+ {
+ var currentResult = Parse(path, expression);
+ if (currentResult.Success)
+ {
+ result = currentResult;
+ break;
+ }
+ }
+
+ if (result != null)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
+ }
+ }
+
+ return result ?? new SeriesPathParserResult();
+ }
+
+ private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
+ {
+ var result = new SeriesPathParserResult();
+
+ var match = expression.Regex.Match(name);
+
+ if (match.Success && match.Groups.Count >= 3)
+ {
+ if (expression.IsNamed)
+ {
+ result.SeriesName = match.Groups["seriesname"].Value;
+ result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesPathParserResult.cs b/Emby.Naming/TV/SeriesPathParserResult.cs
new file mode 100644
index 000000000..44cd2fdfa
--- /dev/null
+++ b/Emby.Naming/TV/SeriesPathParserResult.cs
@@ -0,0 +1,19 @@
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Holder object for <see cref="SeriesPathParser"/> result.
+ /// </summary>
+ public class SeriesPathParserResult
+ {
+ /// <summary>
+ /// Gets or sets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ public string? SeriesName { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether parsing was successful.
+ /// </summary>
+ public bool Success { get; set; }
+ }
+}
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
new file mode 100644
index 000000000..156a03c9e
--- /dev/null
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -0,0 +1,49 @@
+using System.IO;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+ /// <summary>
+ /// Used to resolve information about series from path.
+ /// </summary>
+ public static class SeriesResolver
+ {
+ /// <summary>
+ /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
+ /// Used for removing separators between words, i.e turns "The_show" into "The show" while
+ /// preserving namings like "S.H.O.W".
+ /// </summary>
+ private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
+
+ /// <summary>
+ /// Resolve information about series from path.
+ /// </summary>
+ /// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
+ /// <param name="path">Path to series.</param>
+ /// <returns>SeriesInfo.</returns>
+ public static SeriesInfo Resolve(NamingOptions options, string path)
+ {
+ string seriesName = Path.GetFileName(path);
+
+ SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
+ if (result.Success)
+ {
+ if (!string.IsNullOrEmpty(result.SeriesName))
+ {
+ seriesName = result.SeriesName;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(seriesName))
+ {
+ seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+ }
+
+ return new SeriesInfo(path)
+ {
+ Name = seriesName
+ };
+ }
+ }
+}
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index 4eef3ebc5..a336f8fbd 100644
--- a/Emby.Naming/Video/CleanStringParser.cs
+++ b/Emby.Naming/Video/CleanStringParser.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
@@ -17,38 +16,39 @@ namespace Emby.Naming.Video
/// <param name="expressions">List of regex to parse name and year from.</param>
/// <param name="newName">Parsing result string.</param>
/// <returns>True if parsing was successful.</returns>
- public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
+ public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out string newName)
{
if (string.IsNullOrEmpty(name))
{
- newName = ReadOnlySpan<char>.Empty;
+ newName = string.Empty;
return false;
}
- var len = expressions.Count;
- for (int i = 0; i < len; i++)
+ // Iteratively apply the regexps to clean the string.
+ bool cleaned = false;
+ for (int i = 0; i < expressions.Count; i++)
{
if (TryClean(name, expressions[i], out newName))
{
- return true;
+ cleaned = true;
+ name = newName;
}
}
- newName = ReadOnlySpan<char>.Empty;
- return false;
+ newName = cleaned ? name : string.Empty;
+ return cleaned;
}
- private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
+ private static bool TryClean(string name, Regex expression, out string newName)
{
var match = expression.Match(name);
- int index = match.Index;
- if (match.Success && index != 0)
+ if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned))
{
- newName = name.AsSpan().Slice(0, match.Index);
+ newName = cleaned.Value;
return true;
}
- newName = ReadOnlySpan<char>.Empty;
+ newName = string.Empty;
return false;
}
}
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 1fade985b..fbdca859f 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@@ -10,44 +11,27 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
- public class ExtraResolver
+ public static class ExtraResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ExtraResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
- public ExtraResolver(NamingOptions options)
- {
- _options = options;
- }
+ private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
/// <summary>
/// Attempts to resolve if file is extra.
/// </summary>
/// <param name="path">Path to file.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
- public ExtraResult GetExtraInfo(string path)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
var result = new ExtraResult();
- for (var i = 0; i < _options.VideoExtraRules.Length; i++)
+ for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
- var rule = _options.VideoExtraRules[i];
- if (rule.MediaType == MediaType.Audio)
+ var rule = namingOptions.VideoExtraRules[i];
+ if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+ || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{
- if (!AudioFileParser.IsAudioFile(path, _options))
- {
- continue;
- }
- }
- else if (rule.MediaType == MediaType.Video)
- {
- if (!VideoResolver.IsVideoFile(path, _options))
- {
- continue;
- }
+ continue;
}
var pathSpan = path.AsSpan();
@@ -63,9 +47,10 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
- var filename = Path.GetFileNameWithoutExtension(pathSpan);
+ // Trim the digits from the end of the filename so we can recognize things like -trailer2
+ var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
- if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
+ if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
@@ -75,9 +60,9 @@ namespace Emby.Naming.Video
{
var filename = Path.GetFileName(path);
- var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+ var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
- if (regex.IsMatch(filename))
+ if (isMatch)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
@@ -101,5 +86,66 @@ namespace Emby.Naming.Video
return result;
}
+
+ /// <summary>
+ /// Finds extras matching the video info.
+ /// </summary>
+ /// <param name="files">The list of file video infos.</param>
+ /// <param name="videoInfo">The video to compare against.</param>
+ /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+ /// <returns>A list of video extras for [videoInfo].</returns>
+ public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
+
+ var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
+ var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
+
+ var result = new List<VideoFileInfo>();
+ for (var pos = files.Count - 1; pos >= 0; pos--)
+ {
+ var current = files[pos];
+ // ignore non-extras and multi-file (can this happen?)
+ if (current.ExtraType == null || current.Files.Count > 1)
+ {
+ continue;
+ }
+
+ var currentFile = current.Files[0];
+ var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
+
+ // first check filenames
+ bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
+ || (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
+
+ // then by directory
+ if (!isValid)
+ {
+ // When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
+ var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
+ ? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
+ : Path.GetDirectoryName(currentFile.Path.AsSpan());
+
+ isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (isValid)
+ {
+ result.Add(currentFile);
+ }
+ }
+
+ return result.OrderBy(r => r.Path).ToArray();
+ }
+
+ private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+ {
+ return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+ }
+
+ private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
+ {
+ return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/Emby.Naming/Video/FileStack.cs b/Emby.Naming/Video/FileStack.cs
index 6519db57c..4902e6728 100644
--- a/Emby.Naming/Video/FileStack.cs
+++ b/Emby.Naming/Video/FileStack.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="FileStack"/> class.
/// </summary>
- public FileStack()
+ /// <param name="name">The stack name.</param>
+ /// <param name="isDirectory">Whether the stack files are directories.</param>
+ /// <param name="files">The stack files.</param>
+ public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
{
- Files = new List<string>();
+ Name = name;
+ IsDirectoryStack = isDirectory;
+ Files = files;
}
/// <summary>
- /// Gets or sets name of file stack.
+ /// Gets the name of file stack.
/// </summary>
- public string Name { get; set; } = string.Empty;
+ public string Name { get; }
/// <summary>
- /// Gets or sets list of paths in stack.
+ /// Gets the list of paths in stack.
/// </summary>
- public List<string> Files { get; set; }
+ public IReadOnlyList<string> Files { get; }
/// <summary>
- /// Gets or sets a value indicating whether stack is directory stack.
+ /// Gets a value indicating whether stack is directory stack.
/// </summary>
- public bool IsDirectoryStack { get; set; }
+ public bool IsDirectoryStack { get; }
/// <summary>
/// Helper function to determine if path is in the stack.
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
/// <returns>True if file is in the stack.</returns>
public bool ContainsFile(string file, bool isDirectory)
{
- if (IsDirectoryStack == isDirectory)
+ if (string.IsNullOrEmpty(file))
{
- return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
+ return false;
}
- return false;
+ return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
new file mode 100644
index 000000000..76b487f42
--- /dev/null
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+/// <summary>
+/// Regex based rule for file stacking (eg. disc1, disc2).
+/// </summary>
+public class FileStackRule
+{
+ private readonly Regex _tokenRegex;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FileStackRule"/> class.
+ /// </summary>
+ /// <param name="token">Token.</param>
+ /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
+ public FileStackRule(string token, bool isNumerical)
+ {
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ IsNumerical = isNumerical;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+ /// </summary>
+ public bool IsNumerical { get; }
+
+ /// <summary>
+ /// Match the input against the rule regex.
+ /// </summary>
+ /// <param name="input">The input.</param>
+ /// <param name="result">The part type and number or <c>null</c>.</param>
+ /// <returns>A value indicating whether the input matched the rule.</returns>
+ public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+ {
+ result = null;
+ var match = _tokenRegex.Match(input);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
+ result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+ return true;
+ }
+}
diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs
index 36f65a562..8119a0267 100644
--- a/Emby.Naming/Video/StackResolver.cs
+++ b/Emby.Naming/Video/StackResolver.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using MediaBrowser.Model.IO;
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve <see cref="FileStack"/> from list of paths.
/// </summary>
- public class StackResolver
+ public static class StackResolver
{
- private readonly NamingOptions _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StackResolver"/> class.
- /// </summary>
- /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
- public StackResolver(NamingOptions options)
- {
- _options = options;
- }
-
/// <summary>
/// Resolves only directories from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
- public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
+ public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
}
/// <summary>
/// Resolves only files from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
- public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
+ public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
{
- return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+ return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
}
/// <summary>
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="files">List of paths.</param>
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
- public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
+ public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
{
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
{
foreach (var file in directory)
{
- var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
- stack.Files.Add(file.Path);
+ var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
yield return stack;
}
}
else
{
- var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
- foreach (var file in directory)
- {
- stack.Files.Add(file.Path);
- }
-
+ var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
yield return stack;
}
}
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
/// Resolves videos from paths.
/// </summary>
/// <param name="files">List of paths.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
- public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
+ public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
{
- var list = files
- .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
- .OrderBy(i => i.FullName)
- .ToList();
-
- var expressions = _options.VideoFileStackingRegexes;
+ var potentialFiles = files
+ .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
+ .OrderBy(i => i.FullName);
- for (var i = 0; i < list.Count; i++)
+ var potentialStacks = new Dictionary<string, StackMetadata>();
+ foreach (var file in potentialFiles)
{
- var offset = 0;
-
- var file1 = list[i];
+ var name = file.Name;
+ if (string.IsNullOrEmpty(name))
+ {
+ name = Path.GetFileName(file.FullName);
+ }
- var expressionIndex = 0;
- while (expressionIndex < expressions.Length)
+ for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
{
- var exp = expressions[expressionIndex];
- var stack = new FileStack();
+ var rule = namingOptions.VideoFileStackingRules[i];
+ if (!rule.Match(name, out var stackParsingResult))
+ {
+ continue;
+ }
- // (Title)(Volume)(Ignore)(Extension)
- var match1 = FindMatch(file1, exp, offset);
+ var stackName = stackParsingResult.Value.StackName;
+ var partNumber = stackParsingResult.Value.PartNumber;
+ var partType = stackParsingResult.Value.PartType;
- if (match1.Success)
+ if (!potentialStacks.TryGetValue(stackName, out var stackResult))
{
- var title1 = match1.Groups["title"].Value;
- var volume1 = match1.Groups["volume"].Value;
- var ignore1 = match1.Groups["ignore"].Value;
- var extension1 = match1.Groups["extension"].Value;
+ stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+ potentialStacks[stackName] = stackResult;
+ }
- var j = i + 1;
- while (j < list.Count)
+ if (stackResult.Parts.Count > 0)
+ {
+ if (stackResult.IsDirectory != file.IsDirectory
+ || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+ || stackResult.ContainsPart(partNumber))
{
- var file2 = list[j];
-
- if (file1.IsDirectory != file2.IsDirectory)
- {
- j++;
- continue;
- }
-
- // (Title)(Volume)(Ignore)(Extension)
- var match2 = FindMatch(file2, exp, offset);
-
- if (match2.Success)
- {
- var title2 = match2.Groups[1].Value;
- var volume2 = match2.Groups[2].Value;
- var ignore2 = match2.Groups[3].Value;
- var extension2 = match2.Groups[4].Value;
-
- if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
- {
- if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
- {
- if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
- && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
- {
- if (stack.Files.Count == 0)
- {
- stack.Name = title1 + ignore1;
- stack.IsDirectoryStack = file1.IsDirectory;
- stack.Files.Add(file1.FullName);
- }
-
- stack.Files.Add(file2.FullName);
- }
- else
- {
- // Sequel
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
- {
- // False positive, try again with offset
- offset = match1.Groups[3].Index;
- break;
- }
- else
- {
- // Extension mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // Title mismatch
- offset = 0;
- expressionIndex++;
- break;
- }
- }
- else
- {
- // No match 2, next expression
- offset = 0;
- expressionIndex++;
- break;
- }
-
- j++;
+ continue;
}
- if (j == list.Count)
+ if (rule.IsNumerical != stackResult.IsNumerical)
{
- expressionIndex = expressions.Length;
+ break;
}
}
- else
- {
- // No match 1
- offset = 0;
- expressionIndex++;
- }
- if (stack.Files.Count > 1)
- {
- yield return stack;
- i += stack.Files.Count - 1;
- break;
- }
+ stackResult.Parts.Add(partNumber, file);
+ break;
}
}
- }
- private static string GetRegexInput(FileSystemMetadata file)
- {
- // For directories, dummy up an extension otherwise the expressions will fail
- var input = !file.IsDirectory
- ? file.FullName
- : file.FullName + ".mkv";
+ foreach (var (fileName, stack) in potentialStacks)
+ {
+ if (stack.Parts.Count < 2)
+ {
+ continue;
+ }
- return Path.GetFileName(input);
+ yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+ }
}
- private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
+ private class StackMetadata
{
- var regexInput = GetRegexInput(input);
-
- if (offset < 0 || offset >= regexInput.Length)
+ public StackMetadata(bool isDirectory, bool isNumerical, string partType)
{
- return Match.Empty;
+ Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+ IsDirectory = isDirectory;
+ IsNumerical = isNumerical;
+ PartType = partType;
}
- return regex.Match(regexInput, offset);
+ public Dictionary<string, FileSystemMetadata> Parts { get; }
+
+ public bool IsDirectory { get; }
+
+ public bool IsNumerical { get; }
+
+ public string PartType { get; }
+
+ public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
}
}
}
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index 079987fe8..f7ba606e3 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
-using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path);
- if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index 930fdb33f..8847ee9bc 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
{
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
Name = name;
Files = Array.Empty<VideoFileInfo>();
- Extras = Array.Empty<VideoFileInfo>();
AlternateVersions = Array.Empty<VideoFileInfo>();
}
@@ -40,15 +40,14 @@ namespace Emby.Naming.Video
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
- /// Gets or sets the extras.
- /// </summary>
- /// <value>The extras.</value>
- public IReadOnlyList<VideoFileInfo> Extras { get; set; }
-
- /// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the extra type.
+ /// </summary>
+ public ExtraType? ExtraType { get; set; }
}
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 7da2dcd7a..4fc849256 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -17,29 +16,38 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
- /// <param name="files">List of related video files.</param>
+ /// <param name="videoInfos">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
+ /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
+ public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
- var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
- .OfType<VideoFileInfo>()
- .ToList();
-
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = new StackResolver(namingOptions)
- .Resolve(nonExtras).ToList();
+ var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
- var remainingFiles = videoInfos
- .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
- .ToList();
+ var remainingFiles = new List<VideoFileInfo>();
+ var standaloneMedia = new List<VideoFileInfo>();
+
+ for (var i = 0; i < videoInfos.Count; i++)
+ {
+ var current = videoInfos[i];
+ if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+ {
+ continue;
+ }
+
+ remainingFiles.Add(current);
+ if (current.ExtraType == null)
+ {
+ standaloneMedia.Add(current);
+ }
+ }
var list = new List<VideoInfo>();
@@ -47,27 +55,15 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
-
- var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
-
- if (extras.Count > 0)
- {
- info.Extras = extras;
- }
-
list.Add(info);
}
- var standaloneMedia = remainingFiles
- .Where(i => i.ExtraType == null)
- .ToList();
-
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new[] { media } };
@@ -75,10 +71,6 @@ namespace Emby.Naming.Video
info.Year = info.Files[0].Year;
remainingFiles.Remove(media);
- var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
-
- info.Extras = extras;
-
list.Add(info);
}
@@ -87,58 +79,12 @@ namespace Emby.Naming.Video
list = GetVideosGroupedByVersion(list, namingOptions);
}
- // If there's only one resolved video, use the folder name as well to find extras
- if (list.Count == 1)
- {
- var info = list[0];
- var videoPath = list[0].Files[0].Path;
- var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
-
- if (!parentPath.IsEmpty)
- {
- var folderName = Path.GetFileName(parentPath);
- if (!folderName.IsEmpty)
- {
- var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
- extras.AddRange(info.Extras);
- info.Extras = extras;
- }
- }
-
- // Add the extras that are just based on file name as well
- var extrasByFileName = remainingFiles
- .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
- .ToList();
-
- remainingFiles = remainingFiles
- .Except(extrasByFileName)
- .ToList();
-
- extrasByFileName.AddRange(info.Extras);
- info.Extras = extrasByFileName;
- }
-
- // If there's only one video, accept all trailers
- // Be lenient because people use all kinds of mishmash conventions with trailers.
- if (list.Count == 1)
- {
- var trailers = remainingFiles
- .Where(i => i.ExtraType == ExtraType.Trailer)
- .ToList();
-
- trailers.AddRange(list[0].Extras);
- list[0].Extras = trailers;
-
- remainingFiles = remainingFiles
- .Except(trailers)
- .ToList();
- }
-
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new[] { i },
- Year = i.Year
+ Year = i.Year,
+ ExtraType = i.ExtraType
}));
return list;
@@ -162,6 +108,11 @@ namespace Emby.Naming.Video
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
+ if (video.ExtraType != null)
+ {
+ continue;
+ }
+
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
return videos;
@@ -178,17 +129,14 @@ namespace Emby.Naming.Video
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
- extras.AddRange(video.Extras);
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
- list[0].Extras = extras;
return list;
}
@@ -230,7 +178,7 @@ namespace Emby.Naming.Video
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim().ToString();
+ tmpTestFilename = cleanName.Trim();
}
// The CleanStringParser should have removed common keywords etc.
@@ -238,67 +186,5 @@ namespace Emby.Naming.Video
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
-
- private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
- {
- return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
- }
-
- private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
- {
- if (baseName.IsEmpty)
- {
- return false;
- }
-
- return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
- || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
- }
-
- /// <summary>
- /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
- /// </summary>
- /// <param name="remainingFiles">The list of remaining filenames.</param>
- /// <param name="baseName">The base name to use for the comparison.</param>
- /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
- /// <returns>A list of video extras for [baseName].</returns>
- private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
- {
- return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
- }
-
- /// <summary>
- /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
- /// </summary>
- /// <param name="remainingFiles">The list of remaining filenames.</param>
- /// <param name="firstBaseName">The first base name to use for the comparison.</param>
- /// <param name="secondBaseName">The second base name to use for the comparison.</param>
- /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
- /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
- private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
- {
- var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
- var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
-
- var result = new List<VideoFileInfo>();
- for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
- {
- var file = remainingFiles[pos];
- if (file.ExtraType == null)
- {
- continue;
- }
-
- var filename = file.FileNameWithoutExtension;
- if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
- || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
- {
- result.Add(file);
- remainingFiles.RemoveAt(pos);
- }
- }
-
- return result;
- }
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index c4ac5fdc6..9cadc1465 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -2,7 +2,7 @@ using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Emby.Naming.Common;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <returns>VideoFileInfo.</returns>
- public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
- return Resolve(path, true, namingOptions);
+ return Resolve(path, true, namingOptions, parseName);
}
/// <summary>
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
+ var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path);
@@ -87,9 +88,9 @@ namespace Emby.Naming.Video
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
- && TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
+ && TryCleanString(name, namingOptions, out var newName))
{
- name = newName.ToString();
+ name = newName;
}
}
@@ -138,7 +139,7 @@ namespace Emby.Naming.Video
/// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
- public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
+ public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out string newName)
{
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj
index 5a2aea642..d200682e6 100644
--- a/Emby.Notifications/Emby.Notifications.csproj
+++ b/Emby.Notifications/Emby.Notifications.csproj
@@ -6,13 +6,9 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index 7433d3c8a..a56df7031 100644
--- a/Emby.Notifications/NotificationEntryPoint.cs
+++ b/Emby.Notifications/NotificationEntryPoint.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
@@ -77,7 +78,6 @@ namespace Emby.Notifications
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
- _appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
@@ -105,7 +105,7 @@ namespace Emby.Notifications
var type = entry.Type;
- if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
{
return;
}
@@ -132,25 +132,6 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
- private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
- {
- if (!_appHost.HasUpdateAvailable)
- {
- return;
- }
-
- var type = NotificationType.ApplicationUpdateAvailable.ToString();
-
- var notification = new NotificationRequest
- {
- Description = "Please see jellyfin.org for details.",
- NotificationType = type,
- Name = _localization.GetLocalizedString("NewVersionIsAvailable")
- };
-
- await SendNotification(notification, null).ConfigureAwait(false);
- }
-
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
@@ -325,7 +306,6 @@ namespace Emby.Notifications
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
- _appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 2b6618159..bf6252c19 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,13 +19,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index 4071e4e54..cef82b4d6 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -60,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index d38535634..5ba4749a6 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
{
return _configurations.GetOrAdd(
key,
- (k, configurationManager) =>
+ static (k, configurationManager) =>
{
var file = configurationManager.GetConfigurationFile(k);
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 0308a68e4..f923e59ef 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using System.Linq;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -41,20 +40,19 @@ namespace Emby.Server.Implementations.AppBase
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
- byte[] newBytes = stream.GetBuffer();
- int newBytesLen = (int)stream.Length;
+ Span<byte> newBytes = stream.GetBuffer().AsSpan(0, (int)stream.Length);
// If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
+ if (buffer == null || !newBytes.SequenceEqual(buffer))
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+
// Save it after load in case we got new items
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
- fs.Write(newBytes, 0, newBytesLen);
+ fs.Write(newBytes);
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 82995deb3..8892f7f40 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -18,6 +19,7 @@ using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Drawing;
+using Emby.Naming.Common;
using Emby.Notifications;
using Emby.Photos;
using Emby.Server.Implementations.Archiving;
@@ -38,7 +40,6 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
-using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
@@ -57,9 +58,9 @@ using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -75,7 +76,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@@ -103,7 +103,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@@ -118,6 +117,11 @@ namespace Emby.Server.Implementations
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
+ /// <summary>
+ /// The disposable parts.
+ /// </summary>
+ private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new ();
+
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
@@ -127,7 +131,57 @@ namespace Emby.Server.Implementations
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private string[] _urlPrefixes;
+
+ /// <summary>
+ /// Gets or sets all concrete types.
+ /// </summary>
+ /// <value>All concrete types.</value>
+ private Type[] _allConcreteTypes;
+
+ private DeviceId _deviceId;
+
+ private bool _disposed = false;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+ /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
+ public ApplicationHost(
+ IServerApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IStartupOptions options,
+ IConfiguration startupConfig)
+ {
+ ApplicationPaths = applicationPaths;
+ LoggerFactory = loggerFactory;
+ _startupOptions = options;
+ _startupConfig = startupConfig;
+ _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
+
+ Logger = LoggerFactory.CreateLogger<ApplicationHost>();
+ _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
+
+ ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
+ ApplicationVersionString = ApplicationVersion.ToString(3);
+ ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+
+ _xmlSerializer = new MyXmlSerializer();
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ _pluginManager = new PluginManager(
+ LoggerFactory.CreateLogger<PluginManager>(),
+ this,
+ ConfigurationManager.Configuration,
+ ApplicationPaths.PluginsPath,
+ ApplicationVersion);
+ }
+
+ /// <summary>
+ /// Occurs when [has pending restart changed].
+ /// </summary>
+ public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
@@ -150,25 +204,14 @@ namespace Emby.Server.Implementations
return false;
}
- if (OperatingSystem.Id == OperatingSystemId.Windows
- || OperatingSystem.Id == OperatingSystemId.Darwin)
- {
- return true;
- }
-
- return false;
+ return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
- public INetworkManager NetManager { get; internal set; }
-
- /// <summary>
- /// Occurs when [has pending restart changed].
- /// </summary>
- public event EventHandler HasPendingRestartChanged;
+ public INetworkManager NetManager { get; private set; }
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
@@ -184,35 +227,22 @@ namespace Emby.Server.Implementations
/// </summary>
protected ILogger<ApplicationHost> Logger { get; }
- protected IServiceCollection ServiceCollection { get; }
-
/// <summary>
/// Gets the logger factory.
/// </summary>
protected ILoggerFactory LoggerFactory { get; }
/// <summary>
- /// Gets or sets the application paths.
+ /// Gets the application paths.
/// </summary>
/// <value>The application paths.</value>
- protected IServerApplicationPaths ApplicationPaths { get; set; }
+ protected IServerApplicationPaths ApplicationPaths { get; }
/// <summary>
- /// Gets or sets all concrete types.
- /// </summary>
- /// <value>All concrete types.</value>
- private Type[] _allConcreteTypes;
-
- /// <summary>
- /// The disposable parts.
- /// </summary>
- private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
-
- /// <summary>
- /// Gets or sets the configuration manager.
+ /// Gets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
- public ServerConfigurationManager ConfigurationManager { get; set; }
+ public ServerConfigurationManager ConfigurationManager { get; }
/// <summary>
/// Gets or sets the service provider.
@@ -234,79 +264,6 @@ namespace Emby.Server.Implementations
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
- /// <summary>
- /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
- /// </summary>
- /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
- /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
- /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
- public ApplicationHost(
- IServerApplicationPaths applicationPaths,
- ILoggerFactory loggerFactory,
- IStartupOptions options,
- IConfiguration startupConfig,
- IFileSystem fileSystem,
- IServiceCollection serviceCollection)
- {
- ApplicationPaths = applicationPaths;
- LoggerFactory = loggerFactory;
- _startupOptions = options;
- _startupConfig = startupConfig;
- _fileSystemManager = fileSystem;
- ServiceCollection = serviceCollection;
-
- Logger = LoggerFactory.CreateLogger<ApplicationHost>();
- fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
-
- ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
- ApplicationVersionString = ApplicationVersion.ToString(3);
- ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
-
- _xmlSerializer = new MyXmlSerializer();
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
- _pluginManager = new PluginManager(
- LoggerFactory.CreateLogger<PluginManager>(),
- this,
- ConfigurationManager.Configuration,
- ApplicationPaths.PluginsPath,
- ApplicationVersion);
- }
-
- /// <summary>
- /// Temporary function to migration network settings out of system.xml and into network.xml.
- /// TODO: remove at the point when a fixed migration path has been decided upon.
- /// </summary>
- private void MigrateNetworkConfiguration()
- {
- string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
- if (!File.Exists(path))
- {
- var networkSettings = new NetworkConfiguration();
- ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
- _xmlSerializer.SerializeToFile(networkSettings, path);
- Logger.LogDebug("Successfully migrated network settings.");
- }
- }
-
- public string ExpandVirtualPath(string path)
- {
- var appPaths = ApplicationPaths;
-
- return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
- .Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
- }
-
- public string ReverseVirtualPath(string path)
- {
- var appPaths = ApplicationPaths;
-
- return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
- .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
- }
-
/// <inheritdoc />
public Version ApplicationVersion { get; }
@@ -331,8 +288,6 @@ namespace Emby.Server.Implementations
/// <value>The application name.</value>
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
- private DeviceId _deviceId;
-
public string SystemId
{
get
@@ -346,21 +301,33 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public string Name => ApplicationProductName;
- /// <summary>
- /// Creates an instance of type and resolves all constructor dependencies.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- public object CreateInstance(Type type)
- => ActivatorUtilities.CreateInstance(ServiceProvider, type);
+ private string CertificatePath { get; set; }
- /// <summary>
- /// Creates an instance of type and resolves all constructor dependencies.
- /// </summary>
- /// <typeparam name="T">The type.</typeparam>
- /// <returns>T.</returns>
- public T CreateInstance<T>()
- => ActivatorUtilities.CreateInstance<T>(ServiceProvider);
+ public X509Certificate2 Certificate { get; private set; }
+
+ /// <inheritdoc/>
+ public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
+
+ public string FriendlyName =>
+ string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
+ ? Environment.MachineName
+ : ConfigurationManager.Configuration.ServerName;
+
+ public string ExpandVirtualPath(string path)
+ {
+ var appPaths = ApplicationPaths;
+
+ return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
+ .Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public string ReverseVirtualPath(string path)
+ {
+ var appPaths = ApplicationPaths;
+
+ return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
+ .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
+ }
/// <summary>
/// Creates the instance safe.
@@ -371,7 +338,7 @@ namespace Emby.Server.Implementations
{
_creatingInstances ??= new List<Type>();
- if (_creatingInstances.IndexOf(type) != -1)
+ if (_creatingInstances.Contains(type))
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
@@ -381,7 +348,7 @@ namespace Emby.Server.Implementations
_pluginManager.FailPlugin(type.Assembly);
- throw new ExternalException("DI Loop detected.");
+ throw new TypeLoadException("DI Loop detected");
}
try
@@ -414,8 +381,15 @@ namespace Emby.Server.Implementations
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
-
- return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
+ var numberOfConcreteTypes = _allConcreteTypes.Length;
+ for (var i = 0; i < numberOfConcreteTypes; i++)
+ {
+ var type = _allConcreteTypes[i];
+ if (currentType.IsAssignableFrom(type))
+ {
+ yield return type;
+ }
+ }
}
/// <inheritdoc />
@@ -430,9 +404,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
- lock (_disposableParts)
+ foreach (var part in parts.OfType<IDisposable>())
{
- _disposableParts.AddRange(parts.OfType<IDisposable>());
+ _disposableParts.TryAdd(part, byte.MinValue);
}
}
@@ -451,9 +425,9 @@ namespace Emby.Server.Implementations
if (manageLifetime)
{
- lock (_disposableParts)
+ foreach (var part in parts.OfType<IDisposable>())
{
- _disposableParts.AddRange(parts.OfType<IDisposable>());
+ _disposableParts.TryAdd(part, byte.MinValue);
}
}
@@ -463,6 +437,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
@@ -476,7 +451,7 @@ namespace Emby.Server.Implementations
_mediaEncoder.SetFFmpegPath();
- Logger.LogInformation("ServerId: {0}", SystemId);
+ Logger.LogInformation("ServerId: {ServerId}", SystemId);
var entryPoints = GetExports<IServerEntryPoint>();
@@ -516,14 +491,12 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public void Init()
+ public void Init(IServiceCollection serviceCollection)
{
DiscoverTypes();
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
- // Have to migrate settings here as migration subsystem not yet initialised.
- MigrateNetworkConfiguration();
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
@@ -543,140 +516,133 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
- CertificateInfo = new CertificateInfo
- {
- Path = networkConfiguration.CertificatePath,
- Password = networkConfiguration.CertificatePassword
- };
- Certificate = GetCertificate(CertificateInfo);
+ CertificatePath = networkConfiguration.CertificatePath;
+ Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
- RegisterServices();
+ RegisterServices(serviceCollection);
- _pluginManager.RegisterServices(ServiceCollection);
+ _pluginManager.RegisterServices(serviceCollection);
}
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
/// </summary>
- protected virtual void RegisterServices()
+ /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
+ protected virtual void RegisterServices(IServiceCollection serviceCollection)
{
- ServiceCollection.AddSingleton(_startupOptions);
-
- ServiceCollection.AddMemoryCache();
+ serviceCollection.AddSingleton(_startupOptions);
- ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
- ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
- ServiceCollection.AddSingleton<IApplicationHost>(this);
- ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
- ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+ serviceCollection.AddMemoryCache();
- ServiceCollection.AddSingleton(_fileSystemManager);
- ServiceCollection.AddSingleton<TmdbClientManager>();
+ serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
+ serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
+ serviceCollection.AddSingleton<IApplicationHost>(this);
+ serviceCollection.AddSingleton(_pluginManager);
+ serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- ServiceCollection.AddSingleton(NetManager);
+ serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton<TmdbClientManager>();
- ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
+ serviceCollection.AddSingleton(NetManager);
- ServiceCollection.AddSingleton(_xmlSerializer);
+ serviceCollection.AddSingleton<ITaskManager, TaskManager>();
- ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+ serviceCollection.AddSingleton(_xmlSerializer);
- ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+ serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
- ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+ serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
- ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+ serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
- ServiceCollection.AddSingleton<IZipClient, ZipClient>();
+ serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
- ServiceCollection.AddSingleton<IServerApplicationHost>(this);
- ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+ serviceCollection.AddSingleton<IZipClient, ZipClient>();
- ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+ serviceCollection.AddSingleton<IServerApplicationHost>(this);
+ serviceCollection.AddSingleton(ApplicationPaths);
- ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+ serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
- ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
- ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+ serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+ serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+ serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
+ serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
- ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
- ServiceCollection.AddSingleton<EncodingHelper>();
+ serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+ serviceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
- ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
- ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
- ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
- ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+ serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+ serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+ serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+ serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+ serviceCollection.AddSingleton<NamingOptions>();
- ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
+ serviceCollection.AddSingleton<IMusicManager, MusicManager>();
- ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+ serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
- ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+ serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
- ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
+ serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
- ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+ serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
- ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+ serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
- ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+ serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
- ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+ serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
- ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
-
- ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
+ serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
- ServiceCollection.AddSingleton<IDtoService, DtoService>();
-
- ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
+ serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+ serviceCollection.AddSingleton<IDtoService, DtoService>();
- ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
+ serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
- ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+ serviceCollection.AddSingleton<ISessionManager, SessionManager>();
- ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+ serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
- ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+ serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
- ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+ serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
- ServiceCollection.AddSingleton<LiveTvDtoService>();
- ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+ serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
- ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+ serviceCollection.AddSingleton<LiveTvDtoService>();
+ serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
- ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
+ serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+ serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
- ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
+ serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
- ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+ serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
- ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
- ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
+ serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
- ServiceCollection.AddSingleton<IAuthService, AuthService>();
- ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
+ serviceCollection.AddScoped<ISessionContext, SessionContext>();
- ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+ serviceCollection.AddSingleton<IAuthService, AuthService>();
+ serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
- ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+ serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
- ServiceCollection.AddSingleton<TranscodingJobHelper>();
- ServiceCollection.AddScoped<MediaInfoHelper>();
- ServiceCollection.AddScoped<AudioHelper>();
- ServiceCollection.AddScoped<DynamicHlsHelper>();
+ serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
- ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
+ serviceCollection.AddSingleton<TranscodingJobHelper>();
+ serviceCollection.AddScoped<MediaInfoHelper>();
+ serviceCollection.AddScoped<AudioHelper>();
+ serviceCollection.AddScoped<DynamicHlsHelper>();
+ serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
+ serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
}
/// <summary>
@@ -691,8 +657,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
- ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
-
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
@@ -721,7 +685,7 @@ namespace Emby.Server.Implementations
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
logger.LogInformation("Arguments: {Args}", commandLineArgs);
- logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
+ logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
@@ -731,30 +695,27 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
- private X509Certificate2 GetCertificate(CertificateInfo info)
+ private X509Certificate2 GetCertificate(string path, string password)
{
- var certificateLocation = info?.Path;
-
- if (string.IsNullOrWhiteSpace(certificateLocation))
+ if (string.IsNullOrWhiteSpace(path))
{
return null;
}
try
{
- if (!File.Exists(certificateLocation))
+ if (!File.Exists(path))
{
return null;
}
// Don't use an empty string password
- var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
+ password = string.IsNullOrWhiteSpace(password) ? null : password;
- var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
- // localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
+ var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
- Logger.LogError("No private key included in SSL cert {CertificateLocation}.", certificateLocation);
+ Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
return null;
}
@@ -762,7 +723,7 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error loading cert from {CertificateLocation}", certificateLocation);
+ Logger.LogError(ex, "Error loading cert from {CertificateLocation}", path);
return null;
}
}
@@ -804,8 +765,6 @@ namespace Emby.Server.Implementations
_pluginManager.CreatePlugins();
- _urlPrefixes = GetUrlPrefixes().ToArray();
-
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(),
@@ -873,36 +832,12 @@ namespace Emby.Server.Implementations
}
}
- private CertificateInfo CertificateInfo { get; set; }
-
- public X509Certificate2 Certificate { get; private set; }
-
- private IEnumerable<string> GetUrlPrefixes()
- {
- var hosts = new[] { "+" };
-
- return hosts.SelectMany(i =>
- {
- var prefixes = new List<string>
- {
- "http://" + i + ":" + HttpPort + "/"
- };
-
- if (CertificateInfo != null)
- {
- prefixes.Add("https://" + i + ":" + HttpsPort + "/");
- }
-
- return prefixes;
- });
- }
-
/// <summary>
/// Called when [configuration updated].
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
- protected void OnConfigurationUpdated(object sender, EventArgs e)
+ private void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@@ -911,8 +846,8 @@ namespace Emby.Server.Implementations
if (HttpPort != 0 && HttpsPort != 0)
{
// Need to restart if ports have changed
- if (networkConfiguration.HttpServerPortNumber != HttpPort ||
- networkConfiguration.HttpsPortNumber != HttpsPort)
+ if (networkConfiguration.HttpServerPortNumber != HttpPort
+ || networkConfiguration.HttpsPortNumber != HttpsPort)
{
if (ConfigurationManager.Configuration.IsPortAuthorized)
{
@@ -924,11 +859,6 @@ namespace Emby.Server.Implementations
}
}
- if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
- {
- requiresRestart = true;
- }
-
if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
@@ -952,7 +882,7 @@ namespace Emby.Server.Implementations
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
- && !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
+ && !string.Equals(CertificatePath, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
@@ -970,7 +900,7 @@ namespace Emby.Server.Implementations
}
/// <summary>
- /// Notifies that the kernel that a change has been made that requires a restart.
+ /// Notifies the kernel that a change has been made that requires a restart.
/// </summary>
public void NotifyPendingRestart()
{
@@ -1080,9 +1010,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the system status.
/// </summary>
- /// <param name="source">Where this request originated.</param>
+ /// <param name="request">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
- public SystemInfo GetSystemInfo(IPAddress source)
+ public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
@@ -1098,45 +1028,35 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
- OperatingSystem = OperatingSystem.Id.ToString(),
- OperatingSystemDisplayName = OperatingSystem.Name,
+ OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
+ OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
- HasUpdateAvailable = HasUpdateAvailable,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(source),
+ LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
- EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
};
}
- public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
- => NetManager.GetMacAddresses()
- .Select(i => new WakeOnLanInfo(i))
- .ToList();
-
- public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
- OperatingSystem = OperatingSystem.Id.ToString(),
+ OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(source),
+ LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/>
- public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
-
- /// <inheritdoc/>
- public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
+ public string GetSmartApiUrl(IPAddress remoteAddr)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1145,19 +1065,25 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(ipAddress, out port);
- // If the smartAPI doesn't start with http then treat it as a host or ip.
- if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return smart.Trim('/');
- }
-
+ string smart = NetManager.GetBindInterface(remoteAddr, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
/// <inheritdoc/>
- public string GetSmartApiUrl(HttpRequest request, int? port = null)
+ public string GetSmartApiUrl(HttpRequest request)
{
+ // Return the host in the HTTP request as the API url
+ if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
+ {
+ int? requestPort = request.Host.Port;
+ if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
+ {
+ requestPort = -1;
+ }
+
+ return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
+ }
+
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
@@ -1165,18 +1091,12 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(request, out port);
- // If the smartAPI doesn't start with http then treat it as a host or ip.
- if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return smart.Trim('/');
- }
-
+ string smart = NetManager.GetBindInterface(request, out var port);
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
}
/// <inheritdoc/>
- public string GetSmartApiUrl(string hostname, int? port = null)
+ public string GetSmartApiUrl(string hostname)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1185,50 +1105,41 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(hostname, out port);
-
- // If the smartAPI doesn't start with http then treat it as a host or ip.
- if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return smart.Trim('/');
- }
-
+ string smart = NetManager.GetBindInterface(hostname, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
/// <inheritdoc/>
- public string GetLoopbackHttpApiUrl()
+ public string GetApiUrlForLocalAccess(bool allowHttps = true)
{
- if (NetManager.IsIP6Enabled)
- {
- return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
- }
-
- return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+ // With an empty source, the port will be null
+ string smart = NetManager.GetBindInterface(string.Empty, out _);
+ var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
+ int? port = !allowHttps ? HttpPort : null;
+ return GetLocalApiUrl(smart.Trim('/'), scheme, port);
}
/// <inheritdoc/>
- public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
+ public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{
+ // If the smartAPI doesn't start with http then treat it as a host or ip.
+ if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return hostname.TrimEnd('/');
+ }
+
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash.
return new UriBuilder
{
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
- Host = host,
+ Host = hostname,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/');
}
- public string FriendlyName =>
- string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
- ? Environment.MachineName
- : ConfigurationManager.Configuration.ServerName;
-
- /// <summary>
- /// Shuts down.
- /// </summary>
+ /// <inheritdoc />
public async Task Shutdown()
{
if (IsShuttingDown)
@@ -1252,26 +1163,6 @@ namespace Emby.Server.Implementations
protected abstract void ShutdownInternal();
- public event EventHandler HasUpdateAvailableChanged;
-
- private bool _hasUpdateAvailable;
-
- public bool HasUpdateAvailable
- {
- get => _hasUpdateAvailable;
- set
- {
- var fireEvent = value && !_hasUpdateAvailable;
-
- _hasUpdateAvailable = value;
-
- if (fireEvent)
- {
- HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
- }
- }
- }
-
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
@@ -1286,41 +1177,7 @@ namespace Emby.Server.Implementations
}
}
- public virtual void LaunchUrl(string url)
- {
- if (!CanLaunchWebBrowser)
- {
- throw new NotSupportedException();
- }
-
- var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- FileName = url,
- UseShellExecute = true,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- };
- process.Exited += (sender, args) => ((Process)sender).Dispose();
-
- try
- {
- process.Start();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error launching url: {url}", url);
- throw;
- }
- }
-
- private bool _disposed = false;
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
+ /// <inheritdoc />
public void Dispose()
{
Dispose(true);
@@ -1344,12 +1201,15 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name);
- var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList();
- _disposableParts.Clear();
-
- foreach (var part in parts)
+ foreach (var (part, _) in _disposableParts)
{
- Logger.LogInformation("Disposing {Type}", part.GetType().Name);
+ var partType = part.GetType();
+ if (partType == type)
+ {
+ continue;
+ }
+
+ Logger.LogInformation("Disposing {Type}", partType.Name);
try
{
@@ -1357,19 +1217,14 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
+ Logger.LogError(ex, "Error disposing {Type}", partType.Name);
}
}
+
+ _disposableParts.Clear();
}
_disposed = true;
}
}
-
- internal class CertificateInfo
- {
- public string Path { get; set; }
-
- public string Password { get; set; }
- }
}
diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs
index 591ae547d..6a3b250d2 100644
--- a/Emby.Server.Implementations/Archiving/ZipClient.cs
+++ b/Emby.Server.Implementations/Archiving/ZipClient.cs
@@ -1,11 +1,8 @@
using System.IO;
using MediaBrowser.Model.IO;
-using SharpCompress.Archives.SevenZip;
-using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
-using SharpCompress.Readers.Zip;
namespace Emby.Server.Implementations.Archiving
{
@@ -14,53 +11,6 @@ namespace Emby.Server.Implementations.Archiving
/// </summary>
public class ZipClient : IZipClient
{
- /// <summary>
- /// Extracts all.
- /// </summary>
- /// <param name="sourceFile">The source file.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
- {
- using var fileStream = File.OpenRead(sourceFile);
- ExtractAll(fileStream, targetPath, overwriteExistingFiles);
- }
-
- /// <summary>
- /// Extracts all.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var reader = ReaderFactory.Open(source);
- var options = new ExtractionOptions
- {
- ExtractFullPath = true
- };
-
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
-
- reader.WriteAllToDirectory(targetPath, options);
- }
-
- /// <inheritdoc />
- public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var reader = ZipReader.Open(source);
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- reader.WriteAllToDirectory(targetPath, options);
- }
-
/// <inheritdoc />
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
{
@@ -71,6 +21,7 @@ namespace Emby.Server.Implementations.Archiving
Overwrite = overwriteExistingFiles
};
+ Directory.CreateDirectory(targetPath);
reader.WriteAllToDirectory(targetPath, options);
}
@@ -91,67 +42,5 @@ namespace Emby.Server.Implementations.Archiving
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
}
}
-
- /// <summary>
- /// Extracts all from7z.
- /// </summary>
- /// <param name="sourceFile">The source file.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
- {
- using var fileStream = File.OpenRead(sourceFile);
- ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
- }
-
- /// <summary>
- /// Extracts all from7z.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var archive = SevenZipArchive.Open(source);
- using var reader = archive.ExtractAllEntries();
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- reader.WriteAllToDirectory(targetPath, options);
- }
-
- /// <summary>
- /// Extracts all from tar.
- /// </summary>
- /// <param name="sourceFile">The source file.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
- {
- using var fileStream = File.OpenRead(sourceFile);
- ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
- }
-
- /// <summary>
- /// Extracts all from tar.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
- {
- using var archive = TarArchive.Open(source);
- using var reader = archive.ExtractAllEntries();
- var options = new ExtractionOptions
- {
- ExtractFullPath = true,
- Overwrite = overwriteExistingFiles
- };
-
- reader.WriteAllToDirectory(targetPath, options);
- }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 448f12403..8702691d1 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -10,8 +10,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -102,7 +103,7 @@ namespace Emby.Server.Implementations.Channels
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
- return !(channel is IDisableMediaSourceDisplay);
+ return channel is not IDisableMediaSourceDisplay;
}
/// <inheritdoc />
@@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.Channels
try
{
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
- && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
+ && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
}
catch
{
@@ -541,7 +542,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Channel) },
+ IncludeItemTypes = new[] { BaseItemKind.Channel },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i)).ToArray();
}
@@ -586,7 +587,7 @@ namespace Emby.Server.Implementations.Channels
{
var supportsLatest = provider is ISupportsLatestMedia;
- return new ChannelFeatures
+ return new ChannelFeatures(channel.Name, channel.Id)
{
CanFilter = !features.MaxPageSize.HasValue,
CanSearch = provider is ISearchableChannel,
@@ -596,8 +597,6 @@ namespace Emby.Server.Implementations.Channels
MediaTypes = features.MediaTypes.ToArray(),
SupportsSortOrderToggle = features.SupportsSortOrderToggle,
SupportsLatestMedia = supportsLatest,
- Name = channel.Name,
- Id = channel.Id.ToString("N", CultureInfo.InvariantCulture),
SupportsContentDownloading = features.SupportsContentDownloading,
AutoRefreshLevels = features.AutoRefreshLevels
};
@@ -815,7 +814,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- await using FileStream jsonStream = File.OpenRead(cachePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -838,7 +837,7 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
- await using FileStream jsonStream = File.OpenRead(cachePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
@@ -880,7 +879,7 @@ namespace Emby.Server.Implementations.Channels
}
}
- private async Task CacheResponse(object result, string path)
+ private async Task CacheResponse(ChannelItemResult result, string path)
{
try
{
@@ -1077,14 +1076,6 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true;
}
- // was used for status
- // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
- //{
- // item.ExternalEtag = info.Etag;
- // forceUpdate = true;
- // _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
- //}
-
if (!internalChannelId.Equals(item.ChannelId))
{
forceUpdate = true;
@@ -1145,7 +1136,7 @@ namespace Emby.Server.Implementations.Channels
if (!info.IsLiveStream)
{
- if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+ if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
@@ -1154,7 +1145,7 @@ namespace Emby.Server.Implementations.Channels
}
else
{
- if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+ if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index 2391eed42..b358ba4d5 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Channel) },
+ IncludeItemTypes = new[] { BaseItemKind.Channel },
ExcludeItemIds = installedChannelIds.ToArray()
});
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 82d80fc83..79ef70fff 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -63,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
- public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+ public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
/// <inheritdoc />
- public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+ public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
/// <inheritdoc />
- public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+ public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
private IEnumerable<Folder> FindFolders(string path)
{
@@ -80,14 +78,12 @@ namespace Emby.Server.Implementations.Collections
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
}
- internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
+ internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
{
- var existingFolders = FindFolders(path)
- .ToList();
-
- if (existingFolders.Count > 0)
+ var existingFolder = FindFolders(path).FirstOrDefault();
+ if (existingFolder != null)
{
- return existingFolders[0];
+ return existingFolder;
}
if (!createIfNeeded)
@@ -99,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions
{
- PathInfos = new[] { new MediaPathInfo { Path = path } },
+ PathInfos = new[] { new MediaPathInfo(path) },
EnableRealtimeMonitor = false,
SaveLocalMetadata = true
};
@@ -116,7 +112,7 @@ namespace Emby.Server.Implementations.Collections
return Path.Combine(_appPaths.DataPath, "collections");
}
- private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
+ private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
{
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
}
@@ -164,7 +160,7 @@ namespace Emby.Server.Implementations.Collections
DateCreated = DateTime.UtcNow
};
- parentFolder.AddChild(collection, CancellationToken.None);
+ parentFolder.AddChild(collection);
if (options.ItemIdList.Count > 0)
{
@@ -200,13 +196,12 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
- public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
- => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
+ public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
+ => AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
- var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
- if (collection == null)
+ if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
}
@@ -258,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
/// <inheritdoc />
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
{
- var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-
- if (collection == null)
+ if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
}
@@ -314,11 +307,7 @@ namespace Emby.Server.Implementations.Collections
foreach (var item in items)
{
- if (item is not ISupportsBoxSetGrouping)
- {
- results[item.Id] = item;
- }
- else
+ if (item is ISupportsBoxSetGrouping)
{
var itemId = item.Id;
@@ -342,6 +331,7 @@ namespace Emby.Server.Implementations.Collections
}
var alreadyInResults = false;
+
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
@@ -357,11 +347,13 @@ namespace Emby.Server.Implementations.Collections
}
}
- if (!alreadyInResults)
+ if (alreadyInResults)
{
- results[itemId] = item;
+ continue;
}
}
+
+ results[item.Id] = item;
}
return results.Values;
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 4a9b28085..e9c005cea 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,17 +1,20 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Security.Cryptography;
+using System.Text;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.Cryptography.Constants;
+using static MediaBrowser.Model.Cryptography.Constants;
namespace Emby.Server.Implementations.Cryptography
{
/// <summary>
/// Class providing abstractions over cryptographic functions.
/// </summary>
- public class CryptographyProvider : ICryptoProvider, IDisposable
+ public class CryptographyProvider : ICryptoProvider
{
+ // TODO: remove when not needed for backwards compat
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
@@ -30,107 +33,82 @@ namespace Emby.Server.Implementations.Cryptography
"System.Security.Cryptography.SHA512"
};
- private RandomNumberGenerator _randomNumberGenerator;
-
- private bool _disposed;
+ /// <inheritdoc />
+ public string DefaultHashMethod => "PBKDF2-SHA512";
- /// <summary>
- /// Initializes a new instance of the <see cref="CryptographyProvider"/> class.
- /// </summary>
- public CryptographyProvider()
+ /// <inheritdoc />
+ public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
{
- // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
- // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
- // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
- // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
- _randomNumberGenerator = RandomNumberGenerator.Create();
+ byte[] salt = GenerateSalt();
+ return new PasswordHash(
+ DefaultHashMethod,
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ salt,
+ DefaultIterations,
+ HashAlgorithmName.SHA512,
+ DefaultOutputLength),
+ salt,
+ new Dictionary<string, string>
+ {
+ { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
+ });
}
/// <inheritdoc />
- public string DefaultHashMethod => "PBKDF2";
-
- /// <inheritdoc />
- public IEnumerable<string> GetSupportedHashMethods()
- => _supportedHashMethods;
-
- private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
+ public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
{
- // downgrading for now as we need this library to be dotnetstandard compliant
- // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
- if (method != DefaultHashMethod)
+ if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
{
- throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
+ return hash.Hash.SequenceEqual(
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ hash.Salt,
+ int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+ HashAlgorithmName.SHA1,
+ 32));
}
- using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
- return r.GetBytes(32);
- }
-
- /// <inheritdoc />
- public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
- {
- if (hashMethod == DefaultHashMethod)
+ if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
{
- return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
+ return hash.Hash.SequenceEqual(
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ hash.Salt,
+ int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+ HashAlgorithmName.SHA512,
+ DefaultOutputLength));
}
- if (!_supportedHashMethods.Contains(hashMethod))
+ if (!_supportedHashMethods.Contains(hash.Id))
{
- throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+ throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
}
- using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
- if (salt.Length == 0)
+ using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
+ var bytes = Encoding.UTF8.GetBytes(password.ToArray());
+ if (hash.Salt.Length == 0)
{
- return h.ComputeHash(bytes);
+ return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
}
- byte[] salted = new byte[bytes.Length + salt.Length];
+ byte[] salted = new byte[bytes.Length + hash.Salt.Length];
Array.Copy(bytes, salted, bytes.Length);
- Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
- return h.ComputeHash(salted);
+ hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
+ return hash.Hash.SequenceEqual(h.ComputeHash(salted));
}
/// <inheritdoc />
- public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
- => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
-
- /// <inheritdoc />
public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength);
/// <inheritdoc />
public byte[] GenerateSalt(int length)
{
- byte[] salt = new byte[length];
- _randomNumberGenerator.GetBytes(salt);
+ var salt = new byte[length];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetNonZeroBytes(salt);
return salt;
}
-
- /// <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)
- {
- _randomNumberGenerator.Dispose();
- }
-
- _disposed = true;
- }
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 6f23a0888..5030cbacb 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading;
+using Jellyfin.Extensions;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Data
protected virtual int? CacheSize => null;
/// <summary>
- /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />
+ /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode => "TRUNCATE";
@@ -98,7 +98,7 @@ namespace Emby.Server.Implementations.Data
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
- protected ManagedConnection GetConnection(bool _ = false)
+ protected ManagedConnection GetConnection(bool readOnly = false)
{
WriteLock.Wait();
if (WriteConnection != null)
@@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Data
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
- if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
+ if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
return;
}
@@ -249,55 +249,4 @@ namespace Emby.Server.Implementations.Data
_disposed = true;
}
}
-
- /// <summary>
- /// The disk synchronization mode, controls how aggressively SQLite will write data
- /// all the way out to physical storage.
- /// </summary>
- public enum SynchronousMode
- {
- /// <summary>
- /// SQLite continues without syncing as soon as it has handed data off to the operating system.
- /// </summary>
- Off = 0,
-
- /// <summary>
- /// SQLite database engine will still sync at the most critical moments.
- /// </summary>
- Normal = 1,
-
- /// <summary>
- /// SQLite database engine will use the xSync method of the VFS
- /// to ensure that all content is safely written to the disk surface prior to continuing.
- /// </summary>
- Full = 2,
-
- /// <summary>
- /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
- /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
- /// </summary>
- Extra = 3
- }
-
- /// <summary>
- /// Storage mode used by temporary database files.
- /// </summary>
- public enum TempStoreMode
- {
- /// <summary>
- /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
- /// is used to determine where temporary tables and indices are stored.
- /// </summary>
- Default = 0,
-
- /// <summary>
- /// Temporary tables and indices are stored in a file.
- /// </summary>
- File = 1,
-
- /// <summary>
- /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
- /// </summary>
- Memory = 2
- }
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index afc8966f9..11e33278d 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -7,10 +7,12 @@ using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
- public class ManagedConnection : IDisposable
+ public sealed class ManagedConnection : IDisposable
{
- private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
+
+ private SQLiteDatabaseConnection? _db;
+
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 3289e7609..381eb92a8 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.Data
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
- DateTimeStyles.None).ToUniversalTime();
+ DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
@@ -108,9 +108,9 @@ namespace Emby.Server.Implementations.Data
var dateText = item.ToString();
- if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
+ if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
- result = dateTimeResult.ToUniversalTime();
+ result = dateTimeResult;
return true;
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 9b147b5d7..beae7e243 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -11,10 +11,11 @@ using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
+using Diacritics.Extensions;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -23,7 +24,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -46,6 +46,11 @@ namespace Emby.Server.Implementations.Data
private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
+ private const string SaveItemCommandText =
+ @"replace into TypedBaseItems
+ (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
+
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
private readonly ILocalizationManager _localization;
@@ -55,6 +60,267 @@ namespace Emby.Server.Implementations.Data
private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions;
+ private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
+
+ private static readonly string[] _retrieveItemColumns =
+ {
+ "type",
+ "data",
+ "StartDate",
+ "EndDate",
+ "ChannelId",
+ "IsMovie",
+ "IsSeries",
+ "EpisodeTitle",
+ "IsRepeat",
+ "CommunityRating",
+ "CustomRating",
+ "IndexNumber",
+ "IsLocked",
+ "PreferredMetadataLanguage",
+ "PreferredMetadataCountryCode",
+ "Width",
+ "Height",
+ "DateLastRefreshed",
+ "Name",
+ "Path",
+ "PremiereDate",
+ "Overview",
+ "ParentIndexNumber",
+ "ProductionYear",
+ "OfficialRating",
+ "ForcedSortName",
+ "RunTimeTicks",
+ "Size",
+ "DateCreated",
+ "DateModified",
+ "guid",
+ "Genres",
+ "ParentId",
+ "Audio",
+ "ExternalServiceId",
+ "IsInMixedFolder",
+ "DateLastSaved",
+ "LockedFields",
+ "Studios",
+ "Tags",
+ "TrailerTypes",
+ "OriginalTitle",
+ "PrimaryVersionId",
+ "DateLastMediaAdded",
+ "Album",
+ "CriticRating",
+ "IsVirtualItem",
+ "SeriesName",
+ "SeasonName",
+ "SeasonId",
+ "SeriesId",
+ "PresentationUniqueKey",
+ "InheritedParentalRatingValue",
+ "ExternalSeriesId",
+ "Tagline",
+ "ProviderIds",
+ "Images",
+ "ProductionLocations",
+ "ExtraIds",
+ "TotalBitrate",
+ "ExtraType",
+ "Artists",
+ "AlbumArtists",
+ "ExternalId",
+ "SeriesPresentationUniqueKey",
+ "ShowId",
+ "OwnerId"
+ };
+
+ private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
+
+ private static readonly string[] _mediaStreamSaveColumns =
+ {
+ "ItemId",
+ "StreamIndex",
+ "StreamType",
+ "Codec",
+ "Language",
+ "ChannelLayout",
+ "Profile",
+ "AspectRatio",
+ "Path",
+ "IsInterlaced",
+ "BitRate",
+ "Channels",
+ "SampleRate",
+ "IsDefault",
+ "IsForced",
+ "IsExternal",
+ "Height",
+ "Width",
+ "AverageFrameRate",
+ "RealFrameRate",
+ "Level",
+ "PixelFormat",
+ "BitDepth",
+ "IsAnamorphic",
+ "RefFrames",
+ "CodecTag",
+ "Comment",
+ "NalLengthSize",
+ "IsAvc",
+ "Title",
+ "TimeBase",
+ "CodecTimeBase",
+ "ColorPrimaries",
+ "ColorSpace",
+ "ColorTransfer"
+ };
+
+ private static readonly string _mediaStreamSaveColumnsInsertQuery =
+ $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
+
+ private static readonly string _mediaStreamSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
+
+ private static readonly string[] _mediaAttachmentSaveColumns =
+ {
+ "ItemId",
+ "AttachmentIndex",
+ "Codec",
+ "CodecTag",
+ "Comment",
+ "Filename",
+ "MIMEType"
+ };
+
+ private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
+
+ private static readonly string _mediaAttachmentInsertPrefix;
+
+ private static readonly BaseItemKind[] _programTypes = new[]
+ {
+ BaseItemKind.Program,
+ BaseItemKind.TvChannel,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.LiveTvChannel
+ };
+
+ private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
+ {
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.PhotoAlbum
+ };
+
+ private static readonly BaseItemKind[] _serviceTypes = new[]
+ {
+ BaseItemKind.TvChannel,
+ BaseItemKind.LiveTvChannel
+ };
+
+ private static readonly BaseItemKind[] _startDateTypes = new[]
+ {
+ BaseItemKind.Program,
+ BaseItemKind.LiveTvProgram
+ };
+
+ private static readonly BaseItemKind[] _seriesTypes = new[]
+ {
+ BaseItemKind.Book,
+ BaseItemKind.AudioBook,
+ BaseItemKind.Episode,
+ BaseItemKind.Season
+ };
+
+ private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
+ {
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.PhotoAlbum
+ };
+
+ private static readonly BaseItemKind[] _artistsTypes = new[]
+ {
+ BaseItemKind.Audio,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.AudioBook
+ };
+
+ private static readonly Type[] _knownTypes =
+ {
+ typeof(LiveTvProgram),
+ typeof(LiveTvChannel),
+ typeof(Series),
+ typeof(Audio),
+ typeof(MusicAlbum),
+ typeof(MusicArtist),
+ typeof(MusicGenre),
+ typeof(MusicVideo),
+ typeof(Movie),
+ typeof(Playlist),
+ typeof(AudioBook),
+ typeof(Trailer),
+ typeof(BoxSet),
+ typeof(Episode),
+ typeof(Season),
+ typeof(Series),
+ typeof(Book),
+ typeof(CollectionFolder),
+ typeof(Folder),
+ typeof(Genre),
+ typeof(Person),
+ typeof(Photo),
+ typeof(PhotoAlbum),
+ typeof(Studio),
+ typeof(UserRootFolder),
+ typeof(UserView),
+ typeof(Video),
+ typeof(Year),
+ typeof(Channel),
+ typeof(AggregateFolder)
+ };
+
+ private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
+
+ private static readonly Dictionary<BaseItemKind, string> _baseItemKindNames = new()
+ {
+ { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
+ { BaseItemKind.Audio, typeof(Audio).FullName },
+ { BaseItemKind.AudioBook, typeof(AudioBook).FullName },
+ { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName },
+ { BaseItemKind.Book, typeof(Book).FullName },
+ { BaseItemKind.BoxSet, typeof(BoxSet).FullName },
+ { BaseItemKind.Channel, typeof(Channel).FullName },
+ { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName },
+ { BaseItemKind.Episode, typeof(Episode).FullName },
+ { BaseItemKind.Folder, typeof(Folder).FullName },
+ { BaseItemKind.Genre, typeof(Genre).FullName },
+ { BaseItemKind.Movie, typeof(Movie).FullName },
+ { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName },
+ { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName },
+ { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName },
+ { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName },
+ { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName },
+ { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName },
+ { BaseItemKind.Person, typeof(Person).FullName },
+ { BaseItemKind.Photo, typeof(Photo).FullName },
+ { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName },
+ { BaseItemKind.Playlist, typeof(Playlist).FullName },
+ { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName },
+ { BaseItemKind.Season, typeof(Season).FullName },
+ { BaseItemKind.Series, typeof(Series).FullName },
+ { BaseItemKind.Studio, typeof(Studio).FullName },
+ { BaseItemKind.Trailer, typeof(Trailer).FullName },
+ { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName },
+ { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName },
+ { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName },
+ { BaseItemKind.UserView, typeof(UserView).FullName },
+ { BaseItemKind.Video, typeof(Video).FullName },
+ { BaseItemKind.Year, typeof(Year).FullName }
+ };
+
static SqliteItemRepository()
{
var queryPrefixText = new StringBuilder();
@@ -73,6 +339,12 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
/// </summary>
+ /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
+ /// <exception cref="ArgumentNullException">config is null.</exception>
public SqliteItemRepository(
IServerConfigurationManager config,
IServerApplicationHost appHost,
@@ -109,6 +381,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
+ /// <param name="userDataRepo">The user data repository.</param>
+ /// <param name="userManager">The user manager.</param>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
const string CreateMediaStreamsTableCommand
@@ -148,7 +422,7 @@ namespace Emby.Server.Implementations.Data
"drop index if exists idx_TypedBaseItems",
"drop index if exists idx_mediastreams",
"drop index if exists idx_mediastreams1",
- "drop index if exists idx_"+ChaptersTableName,
+ "drop index if exists idx_" + ChaptersTableName,
"drop index if exists idx_UserDataKeys1",
"drop index if exists idx_UserDataKeys2",
"drop index if exists idx_TypeTopParentId3",
@@ -334,151 +608,12 @@ namespace Emby.Server.Implementations.Data
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
- private static readonly string[] _retriveItemColumns =
- {
- "type",
- "data",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "Name",
- "Path",
- "PremiereDate",
- "Overview",
- "ParentIndexNumber",
- "ProductionYear",
- "OfficialRating",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "guid",
- "Genres",
- "ParentId",
- "Audio",
- "ExternalServiceId",
- "IsInMixedFolder",
- "DateLastSaved",
- "LockedFields",
- "Studios",
- "Tags",
- "TrailerTypes",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "CriticRating",
- "IsVirtualItem",
- "SeriesName",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "PresentationUniqueKey",
- "InheritedParentalRatingValue",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
-
- private static readonly string[] _mediaStreamSaveColumns =
- {
- "ItemId",
- "StreamIndex",
- "StreamType",
- "Codec",
- "Language",
- "ChannelLayout",
- "Profile",
- "AspectRatio",
- "Path",
- "IsInterlaced",
- "BitRate",
- "Channels",
- "SampleRate",
- "IsDefault",
- "IsForced",
- "IsExternal",
- "Height",
- "Width",
- "AverageFrameRate",
- "RealFrameRate",
- "Level",
- "PixelFormat",
- "BitDepth",
- "IsAnamorphic",
- "RefFrames",
- "CodecTag",
- "Comment",
- "NalLengthSize",
- "IsAvc",
- "Title",
- "TimeBase",
- "CodecTimeBase",
- "ColorPrimaries",
- "ColorSpace",
- "ColorTransfer"
- };
-
- private static readonly string _mediaStreamSaveColumnsInsertQuery =
- $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
-
- private static readonly string _mediaStreamSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
-
- private static readonly string[] _mediaAttachmentSaveColumns =
- {
- "ItemId",
- "AttachmentIndex",
- "Codec",
- "CodecTag",
- "Comment",
- "Filename",
- "MIMEType"
- };
-
- private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
-
- private static readonly string _mediaAttachmentInsertPrefix;
-
- private const string SaveItemCommandText =
- @"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
-
/// <summary>
/// Save a standard item in the repo.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">item</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
public void SaveItem(BaseItem item, CancellationToken cancellationToken)
{
if (item == null)
@@ -503,7 +638,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
+ using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
@@ -520,9 +655,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="items">The items.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">
- /// items
- /// or
- /// cancellationToken
+ /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
/// </exception>
public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
{
@@ -1133,15 +1266,25 @@ namespace Emby.Server.Implementations.Data
Path = RestorePath(path.ToString())
};
- if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks))
+ if (long.TryParse(dateModified, NumberStyles.Any, CultureInfo.InvariantCulture, out var ticks)
+ && ticks >= DateTime.MinValue.Ticks
+ && ticks <= DateTime.MaxValue.Ticks)
{
image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
}
+ else
+ {
+ return null;
+ }
- if (Enum.TryParse(imageType.ToString(), true, out ImageType type))
+ if (Enum.TryParse(imageType, true, out ImageType type))
{
image.Type = type;
}
+ else
+ {
+ return null;
+ }
// Optional parameters: width*height*blurhash
if (nextSegment + 1 < value.Length - 1)
@@ -1200,8 +1343,8 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
- /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
public BaseItem RetrieveItem(Guid id)
{
if (id == Guid.Empty)
@@ -1213,7 +1356,7 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection(true))
{
- using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
+ using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1555,7 +1698,6 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(index++, out var audioString))
{
- // TODO Span overload coming in the future https://github.com/dotnet/runtime/issues/1916
if (Enum.TryParse(audioString, true, out ProgramAudio audio))
{
item.Audio = audio;
@@ -1594,18 +1736,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index++, out var lockedFields))
{
- IEnumerable<MetadataField> GetLockedFields(string s)
+ List<MetadataField> fields = null;
+ foreach (var i in lockedFields.AsSpan().Split('|'))
{
- foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
+ if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- yield return parsedValue;
- }
+ (fields ??= new List<MetadataField>()).Add(parsedValue);
}
}
- item.LockedFields = GetLockedFields(lockedFields).ToArray();
+ item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
}
}
@@ -1631,18 +1771,16 @@ namespace Emby.Server.Implementations.Data
{
if (reader.TryGetString(index, out var trailerTypes))
{
- IEnumerable<TrailerType> GetTrailerTypes(string s)
+ List<TrailerType> types = null;
+ foreach (var i in trailerTypes.AsSpan().Split('|'))
{
- foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries))
+ if (Enum.TryParse(i, true, out TrailerType parsedValue))
{
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- yield return parsedValue;
- }
+ (types ??= new List<TrailerType>()).Add(parsedValue);
}
}
- trailer.TrailerTypes = GetTrailerTypes(trailerTypes).ToArray();
+ trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
}
}
@@ -1884,12 +2022,7 @@ namespace Emby.Server.Implementations.Data
return result;
}
- /// <summary>
- /// Gets chapters for an item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>IEnumerable{ChapterInfo}.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <inheritdoc />
public List<ChapterInfo> GetChapters(BaseItem item)
{
CheckDisposed();
@@ -1912,13 +2045,7 @@ namespace Emby.Server.Implementations.Data
}
}
- /// <summary>
- /// Gets a single chapter for an item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="index">The index.</param>
- /// <returns>ChapterInfo.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <inheritdoc />
public ChapterInfo GetChapter(BaseItem item, int index)
{
CheckDisposed();
@@ -1986,6 +2113,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="chapters">The chapters.</param>
public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
{
CheckDisposed();
@@ -2030,7 +2159,7 @@ namespace Emby.Server.Implementations.Data
for (var i = startIndex; i < endIndex; i++)
{
- insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
+ insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
}
insertText.Length -= 1; // Remove last ,
@@ -2085,8 +2214,6 @@ namespace Emby.Server.Implementations.Data
|| query.IsLiked.HasValue;
}
- private readonly ItemFields[] _allFields = Enum.GetValues<ItemFields>();
-
private bool HasField(InternalItemsQuery query, ItemFields name)
{
switch (name)
@@ -2119,26 +2246,9 @@ namespace Emby.Server.Implementations.Data
}
}
- private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Series",
- "Season",
- "MusicAlbum",
- "MusicArtist",
- "PhotoAlbum"
- };
-
- private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Program",
- "TvChannel",
- "LiveTvProgram",
- "LiveTvTvChannel"
- };
-
private bool HasProgramAttributes(InternalItemsQuery query)
{
- if (_programExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2151,15 +2261,9 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
}
- private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "TvChannel",
- "LiveTvTvChannel"
- };
-
private bool HasServiceName(InternalItemsQuery query)
{
- if (_programExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2172,15 +2276,9 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
}
- private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Program",
- "LiveTvProgram"
- };
-
private bool HasStartDate(InternalItemsQuery query)
{
- if (_programExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2200,7 +2298,7 @@ namespace Emby.Server.Implementations.Data
return true;
}
- return query.IncludeItemTypes.Contains("Episode", StringComparer.OrdinalIgnoreCase);
+ return query.IncludeItemTypes.Contains(BaseItemKind.Episode);
}
private bool HasTrailerTypes(InternalItemsQuery query)
@@ -2210,28 +2308,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
+ return query.IncludeItemTypes.Contains(BaseItemKind.Trailer);
}
- private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Series",
- "Season",
- "PhotoAlbum"
- };
-
- private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Audio",
- "MusicAlbum",
- "MusicVideo",
- "AudioBook",
- "AudioPodcast"
- };
-
private bool HasArtistFields(InternalItemsQuery query)
{
- if (_artistExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _artistExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2244,17 +2326,9 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
}
- private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "Book",
- "AudioBook",
- "Episode",
- "Season"
- };
-
private bool HasSeriesFields(InternalItemsQuery query)
{
- if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
+ if (query.ParentType == BaseItemKind.PhotoAlbum)
{
return false;
}
@@ -2269,7 +2343,7 @@ namespace Emby.Server.Implementations.Data
private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
- foreach (var field in _allFields)
+ foreach (var field in _allItemFields)
{
if (!HasField(query, field))
{
@@ -2592,7 +2666,7 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var columns = _retriveItemColumns.ToList();
+ var columns = _retrieveItemColumns.ToList();
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 1024)
.AppendJoin(',', columns)
@@ -2783,7 +2857,7 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
- var columns = _retriveItemColumns.ToList();
+ var columns = _retrieveItemColumns.ToList();
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 512)
.AppendJoin(',', columns)
@@ -3449,8 +3523,8 @@ namespace Emby.Server.Implementations.Data
if (query.IsMovie == true)
{
if (query.IncludeItemTypes.Length == 0
- || query.IncludeItemTypes.Contains(nameof(Movie))
- || query.IncludeItemTypes.Contains(nameof(Trailer)))
+ || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
+ || query.IncludeItemTypes.Contains(BaseItemKind.Trailer))
{
whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
}
@@ -3525,31 +3599,81 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@IsFolder", query.IsFolder);
}
- var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
+ var includeTypes = query.IncludeItemTypes;
// Only specify excluded types if no included types are specified
- if (includeTypes.Length == 0)
+ if (query.IncludeItemTypes.Length == 0)
{
- var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
+ var excludeTypes = query.ExcludeItemTypes;
if (excludeTypes.Length == 1)
{
- whereClauses.Add("type<>@type");
- statement?.TryBind("@type", excludeTypes[0]);
+ if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
+ {
+ whereClauses.Add("type<>@type");
+ statement?.TryBind("@type", excludeTypeName);
+ }
+ else
+ {
+ Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]);
+ }
}
else if (excludeTypes.Length > 1)
{
- var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'"));
- whereClauses.Add($"type not in ({inClause})");
+ var whereBuilder = new StringBuilder("type not in (");
+ foreach (var excludeType in excludeTypes)
+ {
+ if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
+ {
+ whereBuilder
+ .Append('\'')
+ .Append(baseItemKindName)
+ .Append("',");
+ }
+ else
+ {
+ Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType);
+ }
+ }
+
+ // Remove trailing comma.
+ whereBuilder.Length--;
+ whereBuilder.Append(')');
+ whereClauses.Add(whereBuilder.ToString());
}
}
else if (includeTypes.Length == 1)
{
- whereClauses.Add("type=@type");
- statement?.TryBind("@type", includeTypes[0]);
+ if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
+ {
+ whereClauses.Add("type=@type");
+ statement?.TryBind("@type", includeTypeName);
+ }
+ else
+ {
+ Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]);
+ }
}
else if (includeTypes.Length > 1)
{
- var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'"));
- whereClauses.Add($"type in ({inClause})");
+ var whereBuilder = new StringBuilder("type in (");
+ foreach (var includeType in includeTypes)
+ {
+ if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
+ {
+ whereBuilder
+ .Append('\'')
+ .Append(baseItemKindName)
+ .Append("',");
+ }
+ else
+ {
+ Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType);
+ }
+ }
+
+ // Remove trailing comma.
+ whereBuilder.Length--;
+ whereBuilder.Append(')');
+ whereClauses.Add(whereBuilder.ToString());
}
if (query.ChannelIds.Count == 1)
@@ -3873,7 +3997,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue)
{
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series)
{
if (query.IsPlayed.Value)
{
@@ -4723,27 +4847,27 @@ namespace Emby.Server.Implementations.Data
{
var list = new List<string>();
- if (IsTypeInQuery(nameof(Person), query))
+ if (IsTypeInQuery(BaseItemKind.Person, query))
{
list.Add(typeof(Person).FullName);
}
- if (IsTypeInQuery(nameof(Genre), query))
+ if (IsTypeInQuery(BaseItemKind.Genre, query))
{
list.Add(typeof(Genre).FullName);
}
- if (IsTypeInQuery(nameof(MusicGenre), query))
+ if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
{
list.Add(typeof(MusicGenre).FullName);
}
- if (IsTypeInQuery(nameof(MusicArtist), query))
+ if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
{
list.Add(typeof(MusicArtist).FullName);
}
- if (IsTypeInQuery(nameof(Studio), query))
+ if (IsTypeInQuery(BaseItemKind.Studio, query))
{
list.Add(typeof(Studio).FullName);
}
@@ -4751,14 +4875,14 @@ namespace Emby.Server.Implementations.Data
return list;
}
- private bool IsTypeInQuery(string type, InternalItemsQuery query)
+ private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
{
- if (query.ExcludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+ if (query.ExcludeItemTypes.Contains(type))
{
return false;
}
- return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase);
+ return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
}
private string GetCleanValue(string value)
@@ -4798,12 +4922,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
- || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
- || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
- || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
- || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
- || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Contains(BaseItemKind.Episode)
+ || query.IncludeItemTypes.Contains(BaseItemKind.Video)
+ || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
+ || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
+ || query.IncludeItemTypes.Contains(BaseItemKind.Series)
+ || query.IncludeItemTypes.Contains(BaseItemKind.Season))
{
return true;
}
@@ -4811,40 +4935,6 @@ namespace Emby.Server.Implementations.Data
return false;
}
- private static readonly Type[] _knownTypes =
- {
- typeof(LiveTvProgram),
- typeof(LiveTvChannel),
- typeof(Series),
- typeof(Audio),
- typeof(MusicAlbum),
- typeof(MusicArtist),
- typeof(MusicGenre),
- typeof(MusicVideo),
- typeof(Movie),
- typeof(Playlist),
- typeof(AudioBook),
- typeof(Trailer),
- typeof(BoxSet),
- typeof(Episode),
- typeof(Season),
- typeof(Series),
- typeof(Book),
- typeof(CollectionFolder),
- typeof(Folder),
- typeof(Genre),
- typeof(Person),
- typeof(Photo),
- typeof(PhotoAlbum),
- typeof(Studio),
- typeof(UserRootFolder),
- typeof(UserView),
- typeof(Video),
- typeof(Year),
- typeof(Channel),
- typeof(AggregateFolder)
- };
-
public void UpdateInheritedValues()
{
string sql = string.Join(
@@ -4877,7 +4967,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var t in _knownTypes)
{
- dict[t.Name] = t.FullName ;
+ dict[t.Name] = t.FullName;
}
dict["Program"] = typeof(LiveTvProgram).FullName;
@@ -4886,25 +4976,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return dict;
}
- // Not crazy about having this all the way down here, but at least it's in one place
- private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
-
- private string MapIncludeItemTypes(string value)
- {
- if (_types.TryGetValue(value, out string result))
- {
- return result;
- }
-
- if (IsValidType(value))
- {
- return value;
- }
-
- Logger.LogWarning("Unknown item type: {ItemType}", value);
- return null;
- }
-
public void DeleteItem(Guid id)
{
if (id == Guid.Empty)
@@ -5350,7 +5421,7 @@ AND Type = @InternalPersonType)");
stringBuilder.Clear();
}
- List<string> columns = _retriveItemColumns.ToList();
+ List<string> columns = _retrieveItemColumns.ToList();
// Unfortunately we need to add it to columns to ensure the order of the columns in the select
if (!string.IsNullOrEmpty(itemCountColumns))
{
@@ -5568,7 +5639,7 @@ AND Type = @InternalPersonType)");
return result;
}
- private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, string[] typesToCount)
+ private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, BaseItemKind[] typesToCount)
{
var counts = new ItemCounts();
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index ef9af1dcd..107096b5f 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -32,6 +32,9 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="dbLock">The lock to use for database IO.</param>
+ /// <param name="dbConnection">The connection to use for database IO.</param>
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
{
WriteLock.Dispose();
@@ -49,8 +52,8 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
- db.ExecuteAll(string.Join(';', new[] {
-
+ db.ExecuteAll(string.Join(';', new[]
+ {
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
@@ -129,19 +132,17 @@ namespace Emby.Server.Implementations.Data
return list;
}
- /// <summary>
- /// Saves the user data.
- /// </summary>
- public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
- if (internalUserId <= 0)
+ if (userId <= 0)
{
- throw new ArgumentNullException(nameof(internalUserId));
+ throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
@@ -149,22 +150,23 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(key));
}
- PersistUserData(internalUserId, key, userData, cancellationToken);
+ PersistUserData(userId, key, userData, cancellationToken);
}
- public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{
if (userData == null)
{
throw new ArgumentNullException(nameof(userData));
}
- if (internalUserId <= 0)
+ if (userId <= 0)
{
- throw new ArgumentNullException(nameof(internalUserId));
+ throw new ArgumentNullException(nameof(userId));
}
- PersistAllUserData(internalUserId, userData, cancellationToken);
+ PersistAllUserData(userId, userData, cancellationToken);
}
/// <summary>
@@ -174,7 +176,6 @@ namespace Emby.Server.Implementations.Data
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -264,19 +265,19 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the user data.
/// </summary>
- /// <param name="internalUserId">The user id.</param>
+ /// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
- /// key
+ /// key.
/// </exception>
- public UserItemData GetUserData(long internalUserId, string key)
+ public UserItemData GetUserData(long userId, string key)
{
- if (internalUserId <= 0)
+ if (userId <= 0)
{
- throw new ArgumentNullException(nameof(internalUserId));
+ throw new ArgumentNullException(nameof(userId));
}
if (string.IsNullOrEmpty(key))
@@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
- statement.TryBind("@UserId", internalUserId);
+ statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
@@ -301,7 +302,7 @@ namespace Emby.Server.Implementations.Data
}
}
- public UserItemData GetUserData(long internalUserId, List<string> keys)
+ public UserItemData GetUserData(long userId, List<string> keys)
{
if (keys == null)
{
@@ -313,19 +314,19 @@ namespace Emby.Server.Implementations.Data
return null;
}
- return GetUserData(internalUserId, keys[0]);
+ return GetUserData(userId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
- /// <param name="internalUserId"></param>
- /// <returns></returns>
- public List<UserItemData> GetAllUserData(long internalUserId)
+ /// <param name="userId">The internal user id.</param>
+ /// <returns>The list of user item data.</returns>
+ public List<UserItemData> GetAllUserData(long userId)
{
- if (internalUserId <= 0)
+ if (userId <= 0)
{
- throw new ArgumentNullException(nameof(internalUserId));
+ throw new ArgumentNullException(nameof(userId));
}
var list = new List<UserItemData>();
@@ -334,7 +335,7 @@ namespace Emby.Server.Implementations.Data
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
- statement.TryBind("@UserId", internalUserId);
+ statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery())
{
@@ -349,7 +350,8 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Read a row from the specified reader into the provided userData object.
/// </summary>
- /// <param name="reader"></param>
+ /// <param name="reader">The list of result set values.</param>
+ /// <returns>The user item data.</returns>
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
{
var userData = new UserItemData();
diff --git a/Emby.Server.Implementations/Data/SynchronouseMode.cs b/Emby.Server.Implementations/Data/SynchronouseMode.cs
new file mode 100644
index 000000000..cde524e2e
--- /dev/null
+++ b/Emby.Server.Implementations/Data/SynchronouseMode.cs
@@ -0,0 +1,30 @@
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// The disk synchronization mode, controls how aggressively SQLite will write data
+/// all the way out to physical storage.
+/// </summary>
+public enum SynchronousMode
+{
+ /// <summary>
+ /// SQLite continues without syncing as soon as it has handed data off to the operating system.
+ /// </summary>
+ Off = 0,
+
+ /// <summary>
+ /// SQLite database engine will still sync at the most critical moments.
+ /// </summary>
+ Normal = 1,
+
+ /// <summary>
+ /// SQLite database engine will use the xSync method of the VFS
+ /// to ensure that all content is safely written to the disk surface prior to continuing.
+ /// </summary>
+ Full = 2,
+
+ /// <summary>
+ /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
+ /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
+ /// </summary>
+ Extra = 3
+}
diff --git a/Emby.Server.Implementations/Data/TempStoreMode.cs b/Emby.Server.Implementations/Data/TempStoreMode.cs
new file mode 100644
index 000000000..d2427ce47
--- /dev/null
+++ b/Emby.Server.Implementations/Data/TempStoreMode.cs
@@ -0,0 +1,23 @@
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// Storage mode used by temporary database files.
+/// </summary>
+public enum TempStoreMode
+{
+ /// <summary>
+ /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
+ /// is used to determine where temporary tables and indices are stored.
+ /// </summary>
+ Default = 0,
+
+ /// <summary>
+ /// Temporary tables and indices are stored in a file.
+ /// </summary>
+ File = 1,
+
+ /// <summary>
+ /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
+ /// </summary>
+ Memory = 2
+}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index 3d15b3e76..0cfced8be 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -15,9 +15,18 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
-
private readonly object _syncLock = new object();
+ private string _id;
+
+ public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
+ {
+ _appPaths = appPaths;
+ _logger = loggerFactory.CreateLogger<DeviceId>();
+ }
+
+ public string Value => _id ?? (_id = GetDeviceId());
+
private string CachePath => Path.Combine(_appPaths.DataPath, "device.txt");
private string GetCachedId()
@@ -86,15 +95,5 @@ namespace Emby.Server.Implementations.Devices
return id;
}
-
- private string _id;
-
- public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
- {
- _appPaths = appPaths;
- _logger = loggerFactory.CreateLogger<DeviceId>();
- }
-
- public string Value => _id ?? (_id = GetDeviceId());
}
}
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
deleted file mode 100644
index 2637addce..000000000
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Devices
-{
- public class DeviceManager : IDeviceManager
- {
- private readonly IUserManager _userManager;
- private readonly IAuthenticationRepository _authRepo;
- private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
-
- public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
- {
- _userManager = userManager;
- _authRepo = authRepo;
- }
-
- public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
-
- public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
- {
- _capabilitiesMap[deviceId] = capabilities;
- }
-
- public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
- {
- _authRepo.UpdateDeviceOptions(deviceId, options);
-
- DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
- }
-
- public DeviceOptions GetDeviceOptions(string deviceId)
- {
- return _authRepo.GetDeviceOptions(deviceId);
- }
-
- public ClientCapabilities GetCapabilities(string id)
- {
- return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
- ? result
- : new ClientCapabilities();
- }
-
- public DeviceInfo GetDevice(string id)
- {
- var session = _authRepo.Get(new AuthenticationInfoQuery
- {
- DeviceId = id
- }).Items.FirstOrDefault();
-
- var device = session == null ? null : ToDeviceInfo(session);
-
- return device;
- }
-
- public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
- {
- IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
- {
- // UserId = query.UserId
- HasUser = true
- }).Items;
-
- // TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
- if (query.SupportsSync.HasValue)
- {
- var val = query.SupportsSync.Value;
-
- sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
- }
-
- if (!query.UserId.Equals(Guid.Empty))
- {
- var user = _userManager.GetUserById(query.UserId);
-
- sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
- }
-
- var array = sessions.Select(ToDeviceInfo).ToArray();
-
- return new QueryResult<DeviceInfo>(array);
- }
-
- private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
- {
- var caps = GetCapabilities(authInfo.DeviceId);
-
- return new DeviceInfo
- {
- AppName = authInfo.AppName,
- AppVersion = authInfo.AppVersion,
- Id = authInfo.DeviceId,
- LastUserId = authInfo.UserId,
- LastUserName = authInfo.UserName,
- Name = authInfo.DeviceName,
- DateLastActivity = authInfo.DateLastActivity,
- IconUrl = caps?.IconUrl
- };
- }
-
- public bool CanAccessDevice(User user, string deviceId)
- {
- if (user == null)
- {
- throw new ArgumentException("user not found");
- }
-
- if (string.IsNullOrEmpty(deviceId))
- {
- throw new ArgumentNullException(nameof(deviceId));
- }
-
- if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
- {
- return true;
- }
-
- if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
- {
- var capabilities = GetCapabilities(deviceId);
-
- if (capabilities != null && capabilities.SupportsPersistentIdentifier)
- {
- return false;
- }
- }
-
- return true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 7411239a1..a6406827c 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -7,9 +7,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
@@ -51,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
private readonly IMediaSourceManager _mediaSourceManager;
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
- private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
-
public DtoService(
ILogger<DtoService> logger,
ILibraryManager libraryManager,
@@ -75,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
+ private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
+
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
{
- var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
- LivetvManager.AddChannelInfo(list, options, user);
+ LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
}
else if (item is LiveTvProgram)
{
- var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
- var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
- Task.WaitAll(task);
+ LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
}
if (item is IItemByName itemByName
@@ -297,7 +294,7 @@ namespace Emby.Server.Implementations.Dto
path = path.TrimStart('.');
}
- if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
{
fileExtensionContainer = path;
}
@@ -373,6 +370,12 @@ namespace Emby.Server.Implementations.Dto
if (item is MusicAlbum || item is Season || item is Playlist)
{
dto.ChildCount = dto.RecursiveItemCount;
+ var folderChildCount = folder.LinkedChildren.Length;
+ // The default is an empty array, so we can't reliably use the count when it's empty
+ if (folderChildCount > 0)
+ {
+ dto.ChildCount ??= folderChildCount;
+ }
}
if (options.ContainsField(ItemFields.ChildCount))
@@ -420,7 +423,7 @@ namespace Emby.Server.Implementations.Dto
// Just return something so that apps that are expecting a value won't think the folders are empty
if (folder is ICollectionFolder || folder is UserView)
{
- return new Random().Next(1, 10);
+ return Random.Shared.Next(1, 10);
}
return folder.GetChildCount(user);
@@ -467,7 +470,7 @@ namespace Emby.Server.Implementations.Dto
{
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Name = item.Album,
Limit = 1
});
@@ -497,7 +500,7 @@ namespace Emby.Server.Implementations.Dto
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path);
+ _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
return null;
}
}
@@ -507,7 +510,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
- /// <returns>Task.</returns>
private void AttachPeople(BaseItemDto dto, BaseItem item)
{
// Ordering by person type to ensure actors and artists are at the front.
@@ -616,7 +618,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
- /// <returns>Task.</returns>
private void AttachStudios(BaseItemDto dto, BaseItem item)
{
dto.Studios = item.Studios
@@ -757,15 +758,6 @@ namespace Emby.Server.Implementations.Dto
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
}
- if (options.ContainsField(ItemFields.ScreenshotImageTags))
- {
- var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
- if (screenshotLimit > 0)
- {
- dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
- }
- }
-
if (options.ContainsField(ItemFields.Genres))
{
dto.Genres = item.Genres;
@@ -807,7 +799,7 @@ namespace Emby.Server.Implementations.Dto
dto.MediaType = item.MediaType;
- if (!(item is LiveTvProgram))
+ if (item is not LiveTvProgram)
{
dto.LocationType = item.LocationType;
}
@@ -928,9 +920,9 @@ namespace Emby.Server.Implementations.Dto
}
// if (options.ContainsField(ItemFields.MediaSourceCount))
- //{
+ // {
// Songs always have one
- //}
+ // }
}
if (item is IHasArtist hasArtist)
@@ -938,10 +930,10 @@ namespace Emby.Server.Implementations.Dto
dto.Artists = hasArtist.Artists;
// var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
- //{
+ // {
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
- //});
+ // });
// dto.ArtistItems = artistItems.Items
// .Select(i =>
@@ -958,7 +950,7 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = hasArtist.Artists
- //.Except(foundArtists, new DistinctNameComparer())
+ // .Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
// This should not be necessary but we're seeing some cases of it
@@ -990,10 +982,10 @@ namespace Emby.Server.Implementations.Dto
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
// var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
- //{
+ // {
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
- //});
+ // });
// dto.AlbumArtists = artistItems.Items
// .Select(i =>
@@ -1008,7 +1000,7 @@ namespace Emby.Server.Implementations.Dto
// .ToList();
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
- //.Except(foundArtists, new DistinctNameComparer())
+ // .Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
// This should not be necessary but we're seeing some cases of it
@@ -1035,8 +1027,7 @@ namespace Emby.Server.Implementations.Dto
}
// Add video info
- var video = item as Video;
- if (video != null)
+ if (item is Video video)
{
dto.VideoType = video.VideoType;
dto.Video3DFormat = video.Video3DFormat;
@@ -1075,9 +1066,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.MediaStreams))
{
// Add VideoInfo
- var iHasMediaSources = item as IHasMediaSources;
-
- if (iHasMediaSources != null)
+ if (item is IHasMediaSources)
{
MediaStream[] mediaStreams;
@@ -1146,7 +1135,7 @@ namespace Emby.Server.Implementations.Dto
// TODO maybe remove the if statement entirely
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
- episodeSeries = episodeSeries ?? episode.Series;
+ episodeSeries ??= episode.Series;
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
@@ -1159,7 +1148,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
- episodeSeries = episodeSeries ?? episode.Series;
+ episodeSeries ??= episode.Series;
if (episodeSeries != null)
{
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
@@ -1172,7 +1161,7 @@ namespace Emby.Server.Implementations.Dto
{
dto.AirDays = series.AirDays;
dto.AirTime = series.AirTime;
- dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
+ dto.Status = series.Status?.ToString();
}
// Add SeasonInfo
@@ -1185,7 +1174,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.SeriesStudio))
{
- series = series ?? season.Series;
+ series ??= season.Series;
if (series != null)
{
dto.SeriesStudio = series.Studios.FirstOrDefault();
@@ -1196,7 +1185,7 @@ namespace Emby.Server.Implementations.Dto
// TODO maybe remove the if statement entirely
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
- series = series ?? season.Series;
+ series ??= season.Series;
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
@@ -1283,7 +1272,7 @@ namespace Emby.Server.Implementations.Dto
var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
- if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
+ if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
{
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
}
@@ -1316,9 +1305,12 @@ namespace Emby.Server.Implementations.Dto
var imageTags = dto.ImageTags;
- while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
- (parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
+ while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
+ || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
+ || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
+ || parent is Series)
{
+ parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
if (parent == null)
{
break;
@@ -1348,7 +1340,7 @@ namespace Emby.Server.Implementations.Dto
}
}
- if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
+ if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
@@ -1398,7 +1390,6 @@ namespace Emby.Server.Implementations.Dto
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
- /// <returns>Task.</returns>
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
{
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
@@ -1413,44 +1404,27 @@ namespace Emby.Server.Implementations.Dto
return null;
}
- ImageDimensions size;
-
- var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
-
- if (defaultAspectRatio > 0)
- {
- return defaultAspectRatio;
- }
-
if (!imageInfo.IsLocalFile)
{
- return null;
+ return item.GetDefaultPrimaryImageAspectRatio();
}
try
{
- size = _imageProcessor.GetImageDimensions(item, imageInfo);
-
- if (size.Width <= 0 || size.Height <= 0)
+ var size = _imageProcessor.GetImageDimensions(item, imageInfo);
+ var width = size.Width;
+ var height = size.Height;
+ if (width > 0 && height > 0)
{
- return null;
+ return (double)width / height;
}
}
catch (Exception ex)
{
- _logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
- return null;
- }
-
- var width = size.Width;
- var height = size.Height;
-
- if (width <= 0 || height <= 0)
- {
- return null;
+ _logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
}
- return (double)width / height;
+ return item.GetDefaultPrimaryImageAspectRatio();
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 9c90de1ed..329a84acb 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -23,17 +23,18 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
- <PackageReference Include="Mono.Nat" Version="3.0.1" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
- <PackageReference Include="sharpcompress" Version="0.28.3" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
+ <PackageReference Include="Mono.Nat" Version="3.0.2" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
+ <PackageReference Include="sharpcompress" Version="0.30.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
- <PackageReference Include="DotNet.Glob" Version="3.1.2" />
+ <PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
@@ -41,15 +42,11 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
- <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 0a4efd73c..06e57ad12 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -9,12 +9,10 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Dlna;
using Microsoft.Extensions.Logging;
using Mono.Nat;
@@ -28,7 +26,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IServerApplicationHost _appHost;
private readonly ILogger<ExternalPortForwarding> _logger;
private readonly IServerConfigurationManager _config;
- private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
@@ -43,17 +40,14 @@ namespace Emby.Server.Implementations.EntryPoints
/// <param name="logger">The logger.</param>
/// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param>
- /// <param name="deviceDiscovery">The device discovery.</param>
public ExternalPortForwarding(
ILogger<ExternalPortForwarding> logger,
IServerApplicationHost appHost,
- IServerConfigurationManager config,
- IDeviceDiscovery deviceDiscovery)
+ IServerConfigurationManager config)
{
_logger = logger;
_appHost = appHost;
_config = config;
- _deviceDiscovery = deviceDiscovery;
}
private string GetConfigIdentifier()
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 5bb4100ba..331de45c1 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints
private static bool EnableRefreshMessage(BaseItem item)
{
- if (!(item is Folder folder))
+ if (item is not Folder folder)
{
return false;
}
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
return false;
}
- if (item is IItemByName && !(item is MusicArtist))
+ if (item is IItemByName && item is not MusicArtist)
{
return false;
}
@@ -436,7 +436,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Translates the physical item to user library.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of item.</typeparam>
/// <param name="item">The item.</param>
/// <param name="user">The user.</param>
/// <param name="includeIfNotFound">if set to <c>true</c> [include if not found].</param>
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 2e72b18f5..feaccf9fa 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -37,6 +37,9 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
/// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
+ /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
public UdpServerEntryPoint(
ILogger<UdpServerEntryPoint> logger,
IServerApplicationHost appHost,
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 9afabf527..1d04f3da3 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -17,13 +17,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
- public AuthorizationInfo Authenticate(HttpRequest request)
+ public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{
- var auth = _authorizationContext.GetAuthorizationInfo(request);
+ var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken)
{
- throw new AuthenticationException("Request does not contain a token.");
+ return auth;
}
if (!auth.IsAuthenticated)
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index c375f36ce..a7647caf9 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
@@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
- public SessionInfo GetSession(HttpContext requestContext)
+ public async Task<SessionInfo> GetSession(HttpContext requestContext)
{
- var authorization = _authContext.GetAuthorizationInfo(requestContext);
+ var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
+ return await _sessionManager.LogSessionActivity(
+ authorization.Client,
+ authorization.Version,
+ authorization.DeviceId,
+ authorization.Device,
+ requestContext.GetNormalizedRemoteIp().ToString(),
+ user).ConfigureAwait(false);
}
- public SessionInfo GetSession(object requestContext)
+ public Task<SessionInfo> GetSession(object requestContext)
{
return GetSession((HttpContext)requestContext);
}
- public User? GetUser(HttpContext requestContext)
+ public async Task<User?> GetUser(HttpContext requestContext)
{
- var session = GetSession(requestContext);
+ var session = await GetSession(requestContext).ConfigureAwait(false);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
- public User? GetUser(object requestContext)
+ public Task<User?> GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 362f51e50..0eca2a608 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -7,7 +7,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
public event EventHandler<EventArgs>? Closed;
/// <summary>
- /// Gets or sets the remote end point.
+ /// Gets the remote end point.
/// </summary>
public IPAddress? RemoteEndPoint { get; }
@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer
public DateTime LastKeepAliveDate { get; set; }
/// <summary>
- /// Gets or sets the query string.
+ /// Gets the query string.
/// </summary>
/// <value>The query string.</value>
public IQueryCollection QueryString { get; }
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -150,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
- } while (
- (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+ }
+ while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 861c0a95e..e99876dce 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
- _ = _authService.Authenticate(context.Request);
+ var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
+ if (!authorizationInfo.IsAuthenticated)
+ {
+ throw new SecurityException("Token is required");
+ }
+
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 47a83d77c..e62361c1e 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
- public class FileRefresher : IDisposable
+ public sealed class FileRefresher : IDisposable
{
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object();
- private Timer _timer;
+ private Timer? _timer;
private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
AddPath(path);
}
- public event EventHandler<EventArgs> Completed;
+ public event EventHandler<EventArgs>? Completed;
public string Path { get; private set; }
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
RestartTimer();
}
- private void OnTimerCallback(object state)
+ private void OnTimerCallback(object? state)
{
List<string> paths;
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
try
{
- ProcessPathChanges(paths.ToList());
+ ProcessPathChanges(paths);
}
catch (Exception ex)
{
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
private void ProcessPathChanges(List<string> paths)
{
- var itemsToRefresh = paths
+ IEnumerable<BaseItem> itemsToRefresh = paths
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAffectedBaseItem)
.Where(item => item != null)
- .GroupBy(x => x.Id)
- .Select(x => x.First());
+ .GroupBy(x => x!.Id) // Removed null values in the previous .Where()
+ .Select(x => x.First())!;
foreach (var item in itemsToRefresh)
{
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="path">The path.</param>
/// <returns>BaseItem.</returns>
- private BaseItem GetAffectedBaseItem(string path)
+ private BaseItem? GetAffectedBaseItem(string path)
{
- BaseItem item = null;
+ BaseItem? item = null;
while (item == null && !string.IsNullOrEmpty(path))
{
item = _libraryManager.FindByPath(path, null);
- path = System.IO.Path.GetDirectoryName(path);
+ path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
}
if (item != null)
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index aa80bccd7..9fcc7fe59 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -42,6 +42,25 @@ namespace Emby.Server.Implementations.IO
private bool _disposed = false;
/// <summary>
+ /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public LibraryMonitor(
+ ILogger<LibraryMonitor> logger,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager configurationManager,
+ IFileSystem fileSystem)
+ {
+ _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>
@@ -95,21 +114,6 @@ namespace Emby.Server.Implementations.IO
}
}
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
- /// </summary>
- public LibraryMonitor(
- ILogger<LibraryMonitor> logger,
- ILibraryManager libraryManager,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _configurationManager = configurationManager;
- _fileSystem = fileSystem;
- }
-
private bool IsLibraryMonitorEnabled(BaseItem item)
{
if (item is BasePluginFolder)
@@ -199,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="lst">The LST.</param>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">path</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
{
if (string.IsNullOrEmpty(path))
@@ -263,7 +267,7 @@ namespace Emby.Server.Implementations.IO
if (_fileSystemWatchers.TryAdd(path, newWatcher))
{
newWatcher.EnableRaisingEvents = true;
- _logger.LogInformation("Watching directory " + path);
+ _logger.LogInformation("Watching directory {Path}", path);
}
else
{
@@ -272,7 +276,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error watching path: {path}", path);
+ _logger.LogError(ex, "Error watching path: {Path}", path);
}
});
}
@@ -445,12 +449,12 @@ namespace Emby.Server.Implementations.IO
}
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
- newRefresher.Completed += NewRefresher_Completed;
+ newRefresher.Completed += OnNewRefresherCompleted;
_activeRefreshers.Add(newRefresher);
}
}
- private void NewRefresher_Completed(object sender, EventArgs e)
+ private void OnNewRefresherCompleted(object sender, EventArgs e)
{
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
@@ -477,6 +481,7 @@ namespace Emby.Server.Implementations.IO
{
lock (_activeRefreshers)
{
+ refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
_activeRefreshers.Remove(refresher);
}
@@ -488,6 +493,7 @@ namespace Emby.Server.Implementations.IO
{
foreach (var refresher in _activeRefreshers.ToList())
{
+ refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 64d802457..777cd2cd4 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -1,17 +1,12 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO
{
@@ -20,20 +15,26 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
- protected ILogger<ManagedFileSystem> Logger;
+ private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
- private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
+ /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths)
{
- Logger = logger;
+ _logger = logger;
_tempPath = applicationPaths.TempDirectory;
}
+ /// <inheritdoc />
public virtual void AddShortcutHandler(IShortcutHandler handler)
{
_shortcutHandlers.Add(handler);
@@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
- /// <exception cref="ArgumentNullException">filename</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual bool IsShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -61,7 +62,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">filename</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="filename"/> is <c>null</c>.</exception>
public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
@@ -75,6 +76,7 @@ namespace Emby.Server.Implementations.IO
return handler?.Resolve(filename);
}
+ /// <inheritdoc />
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
// path is actually a stream
@@ -236,9 +238,9 @@ namespace Emby.Server.Implementations.IO
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
- //{
+ // {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
- //}
+ // }
if (info is FileInfo fileInfo)
{
@@ -249,15 +251,15 @@ namespace Emby.Server.Implementations.IO
{
try
{
- using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
- result.Length = thisFileStream.Length;
+ result.Length = RandomAccess.GetLength(fileHandle);
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
- Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
+ _logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
@@ -346,7 +348,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
+ _logger.LogError(ex, "Error determining CreationTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -361,11 +363,13 @@ namespace Emby.Server.Implementations.IO
return GetCreationTimeUtc(GetFileSystemInfo(path));
}
+ /// <inheritdoc />
public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info)
{
return info.CreationTimeUtc;
}
+ /// <inheritdoc />
public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
{
return info.LastWriteTimeUtc;
@@ -385,7 +389,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
+ _logger.LogError(ex, "Error determining LastAccessTimeUtc for {FullName}", info.FullName);
return DateTime.MinValue;
}
}
@@ -400,9 +404,10 @@ namespace Emby.Server.Implementations.IO
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
}
+ /// <inheritdoc />
public virtual void SetHidden(string path, bool isHidden)
{
- if (OperatingSystem.Id != OperatingSystemId.Windows)
+ if (!OperatingSystem.IsWindows())
{
return;
}
@@ -424,9 +429,10 @@ namespace Emby.Server.Implementations.IO
}
}
- public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
+ /// <inheritdoc />
+ public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
{
- if (OperatingSystem.Id != OperatingSystemId.Windows)
+ if (!OperatingSystem.IsWindows())
{
return;
}
@@ -438,16 +444,16 @@ namespace Emby.Server.Implementations.IO
return;
}
- if (info.IsReadOnly == isReadOnly && info.IsHidden == isHidden)
+ if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
{
return;
}
var attributes = File.GetAttributes(path);
- if (isReadOnly)
+ if (readOnly)
{
- attributes = attributes | FileAttributes.ReadOnly;
+ attributes |= FileAttributes.ReadOnly;
}
else
{
@@ -456,7 +462,7 @@ namespace Emby.Server.Implementations.IO
if (isHidden)
{
- attributes = attributes | FileAttributes.Hidden;
+ attributes |= FileAttributes.Hidden;
}
else
{
@@ -501,6 +507,7 @@ namespace Emby.Server.Implementations.IO
File.Copy(temp1, file2, true);
}
+ /// <inheritdoc />
public virtual bool ContainsSubPath(string parentPath, string path)
{
if (string.IsNullOrEmpty(parentPath))
@@ -518,6 +525,7 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
+ /// <inheritdoc />
public virtual string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
@@ -533,6 +541,7 @@ namespace Emby.Server.Implementations.IO
return Path.TrimEndingDirectorySeparator(path);
}
+ /// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
if (path1 == null && path2 == null)
@@ -551,6 +560,7 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
+ /// <inheritdoc />
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
{
if (info.IsDirectory)
@@ -561,11 +571,11 @@ namespace Emby.Server.Implementations.IO
return Path.GetFileNameWithoutExtension(info.FullName);
}
+ /// <inheritdoc />
public virtual bool IsPathFile(string path)
{
- // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
- if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
- !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+ if (path.Contains("://", StringComparison.OrdinalIgnoreCase)
+ && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -573,17 +583,23 @@ namespace Emby.Server.Implementations.IO
return true;
}
+ /// <inheritdoc />
public virtual void DeleteFile(string path)
{
SetAttributes(path, false, false);
File.Delete(path);
}
+ /// <inheritdoc />
public virtual List<FileSystemMetadata> GetDrives()
{
// check for ready state to avoid waiting for drives to timeout
// some drives on linux have no actual size or are used for other purposes
- return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram)
+ return DriveInfo.GetDrives()
+ .Where(
+ d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
+ && d.IsReady
+ && d.TotalSize != 0)
.Select(d => new FileSystemMetadata
{
Name = d.Name,
@@ -592,16 +608,19 @@ namespace Emby.Server.Implementations.IO
}).ToList();
}
+ /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
+ /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{
return GetFiles(path, null, false, recursive);
}
+ /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -632,6 +651,7 @@ namespace Emby.Server.Implementations.IO
return ToMetadata(files);
}
+ /// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
var directoryInfo = new DirectoryInfo(path);
@@ -645,16 +665,19 @@ namespace Emby.Server.Implementations.IO
return infos.Select(GetFileSystemMetadata);
}
+ /// <inheritdoc />
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
+ /// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
{
return GetFilePaths(path, null, false, recursive);
}
+ /// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -685,6 +708,7 @@ namespace Emby.Server.Implementations.IO
return files;
}
+ /// <inheritdoc />
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index a430b9e72..3769ae4dd 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,7 +1,8 @@
-#pragma warning disable CS1591
-
namespace Emby.Server.Implementations
{
+ /// <summary>
+ /// Specifies the contract for server startup options.
+ /// </summary>
public interface IStartupOptions
{
/// <summary>
@@ -10,7 +11,7 @@ namespace Emby.Server.Implementations
string? FFmpegPath { get; }
/// <summary>
- /// Gets the value of the --service command line option.
+ /// Gets a value indicating whether to run as service by the --service command line option.
/// </summary>
bool IsService { get; }
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 833fb0b7a..758986945 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Images
public int Order => 0;
- protected virtual bool Supports(BaseItem _) => true;
+ protected virtual bool Supports(BaseItem item) => true;
public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
@@ -65,13 +65,13 @@ namespace Emby.Server.Implementations.Images
if (SupportedImages.Contains(ImageType.Primary))
{
var primaryResult = await FetchAsync(item, ImageType.Primary, options, cancellationToken).ConfigureAwait(false);
- updateType = updateType | primaryResult;
+ updateType |= primaryResult;
}
if (SupportedImages.Contains(ImageType.Thumb))
{
var thumbResult = await FetchAsync(item, ImageType.Thumb, options, cancellationToken).ConfigureAwait(false);
- updateType = updateType | thumbResult;
+ updateType |= thumbResult;
}
return updateType;
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
new file mode 100644
index 000000000..1c69056d2
--- /dev/null
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -0,0 +1,67 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
+ where T : Folder, new()
+ {
+ private readonly ILibraryManager _libraryManager;
+
+ public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+ {
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Parent = item,
+ DtoOptions = new DtoOptions(true),
+ ImageTypes = new ImageType[] { ImageType.Primary },
+ OrderBy = new (string, SortOrder)[]
+ {
+ (ItemSortBy.IsFolder, SortOrder.Ascending),
+ (ItemSortBy.SortName, SortOrder.Ascending)
+ },
+ Limit = 1
+ });
+ }
+
+ protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
+ {
+ return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
+ }
+
+ protected override bool Supports(BaseItem item)
+ {
+ return item is T;
+ }
+
+ protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
+ {
+ if (item is MusicAlbum)
+ {
+ return false;
+ }
+
+ return base.HasChangedByDate(item, image);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index ff5f26ce0..7e12ebb08 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -28,35 +28,35 @@ namespace Emby.Server.Implementations.Images
var view = (CollectionFolder)item;
var viewType = view.CollectionType;
- string[] includeItemTypes;
+ BaseItemKind[] includeItemTypes;
- if (string.Equals(viewType, CollectionType.Movies))
+ if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "Movie" };
+ includeItemTypes = new[] { BaseItemKind.Movie };
}
- else if (string.Equals(viewType, CollectionType.TvShows))
+ else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "Series" };
+ includeItemTypes = new[] { BaseItemKind.Series };
}
- else if (string.Equals(viewType, CollectionType.Music))
+ else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "MusicAlbum" };
+ includeItemTypes = new[] { BaseItemKind.MusicAlbum };
}
- else if (string.Equals(viewType, CollectionType.Books))
+ else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "Book", "AudioBook" };
+ includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
}
- else if (string.Equals(viewType, CollectionType.BoxSets))
+ else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "BoxSet" };
+ includeItemTypes = new[] { BaseItemKind.BoxSet };
}
- else if (string.Equals(viewType, CollectionType.HomeVideos) || string.Equals(viewType, CollectionType.Photos))
+ else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "Video", "Photo" };
+ includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
}
else
{
- includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" };
+ includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
}
var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 900b3fd9c..575680653 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -6,6 +6,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -34,14 +36,14 @@ namespace Emby.Server.Implementations.Images
var view = (UserView)item;
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
- var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
var result = view.GetItemList(new InternalItemsQuery
{
User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null,
CollapseBoxSetItems = false,
Recursive = recursive,
- ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" },
+ ExcludeItemTypes = new[] { BaseItemKind.UserView, BaseItemKind.CollectionFolder, BaseItemKind.Person },
DtoOptions = new DtoOptions(false)
});
diff --git a/Emby.Server.Implementations/Images/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 859017f86..4376bd356 100644
--- a/Emby.Server.Implementations/Images/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -2,69 +2,16 @@
#pragma warning disable CS1591
-using System.Collections.Generic;
-using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
- public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
- where T : Folder, new()
- {
- protected ILibraryManager _libraryManager;
-
- public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- _libraryManager = libraryManager;
- }
-
- protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
- {
- return _libraryManager.GetItemList(new InternalItemsQuery
- {
- Parent = item,
- DtoOptions = new DtoOptions(true),
- ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new System.ValueTuple<string, SortOrder>[]
- {
- new System.ValueTuple<string, SortOrder>(ItemSortBy.IsFolder, SortOrder.Ascending),
- new System.ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)
- },
- Limit = 1
- });
- }
-
- protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
- {
- return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
- }
-
- protected override bool Supports(BaseItem item)
- {
- return item is T;
- }
-
- protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
- {
- if (item is MusicAlbum)
- {
- return false;
- }
-
- return base.HasChangedByDate(item, image);
- }
- }
-
public class FolderImageProvider : BaseFolderImageProvider<Folder>
{
public FolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
@@ -87,20 +34,4 @@ namespace Emby.Server.Implementations.Images
return true;
}
}
-
- public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
- {
- public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
- {
- }
- }
-
- public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
- {
- public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
- : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
- {
- }
- }
}
diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 6da431c68..968bf5fa3 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -8,9 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -20,46 +17,6 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
/// <summary>
- /// Class MusicGenreImageProvider.
- /// </summary>
- public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
- {
- /// <summary>
- /// The library manager.
- /// </summary>
- private readonly ILibraryManager _libraryManager;
-
- public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- _libraryManager = libraryManager;
- }
-
- /// <summary>
- /// Get children objects used to create an music genre image.
- /// </summary>
- /// <param name="item">The music genre used to create the image.</param>
- /// <returns>Any relevant children objects.</returns>
- protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
- {
- return _libraryManager.GetItemList(new InternalItemsQuery
- {
- Genres = new[] { item.Name },
- IncludeItemTypes = new[]
- {
- nameof(MusicAlbum),
- nameof(MusicVideo),
- nameof(Audio)
- },
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
- Limit = 4,
- Recursive = true,
- ImageTypes = new[] { ImageType.Primary },
- DtoOptions = new DtoOptions(false)
- });
- }
- }
-
- /// <summary>
/// Class GenreImageProvider.
/// </summary>
public class GenreImageProvider : BaseDynamicImageProvider<Genre>
@@ -84,7 +41,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Genres = new[] { item.Name },
- IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
+ IncludeItemTypes = new[] { BaseItemKind.Series, BaseItemKind.Movie },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
Recursive = true,
diff --git a/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
new file mode 100644
index 000000000..ce8367363
--- /dev/null
+++ b/Emby.Server.Implementations/Images/MusicAlbumImageProvider.cs
@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class MusicAlbumImageProvider : BaseFolderImageProvider<MusicAlbum>
+ {
+ public MusicAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
new file mode 100644
index 000000000..31f053f06
--- /dev/null
+++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
@@ -0,0 +1,59 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ /// <summary>
+ /// Class MusicGenreImageProvider.
+ /// </summary>
+ public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
+ {
+ /// <summary>
+ /// The library manager.
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Get children objects used to create an music genre image.
+ /// </summary>
+ /// <param name="item">The music genre used to create the image.</param>
+ /// <returns>Any relevant children objects.</returns>
+ protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+ {
+ return _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ Genres = new[] { item.Name },
+ IncludeItemTypes = new[]
+ {
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Audio
+ },
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
+ Limit = 4,
+ Recursive = true,
+ ImageTypes = new[] { ImageType.Primary },
+ DtoOptions = new DtoOptions(false)
+ });
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs b/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs
new file mode 100644
index 000000000..1ddb4c757
--- /dev/null
+++ b/Emby.Server.Implementations/Images/PhotoAlbumImageProvider.cs
@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class PhotoAlbumImageProvider : BaseFolderImageProvider<PhotoAlbum>
+ {
+ public PhotoAlbumImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index c7d113963..e558fbe27 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -1,8 +1,9 @@
using System;
using System.IO;
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO;
@@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
{
- private readonly ILibraryManager _libraryManager;
+ private readonly NamingOptions _namingOptions;
private readonly IServerApplicationPaths _serverApplicationPaths;
/// <summary>
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
/// </summary>
- /// <param name="libraryManager">The library manager.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="serverApplicationPaths">The server application paths.</param>
- public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
+ public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths)
{
- _libraryManager = libraryManager;
+ _namingOptions = namingOptions;
_serverApplicationPaths = serverApplicationPaths;
}
@@ -53,20 +54,10 @@ namespace Emby.Server.Implementations.Library
{
if (parent != null)
{
- // Ignore trailer folders but allow it at the collection level
- if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)
- && !(parent is AggregateFolder)
- && !(parent is UserRootFolder))
- {
- return true;
- }
-
- if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
-
- if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+ // Ignore extras folders but allow it at the collection level
+ if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
+ && parent is not AggregateFolder
+ && parent is not UserRootFolder)
{
return true;
}
@@ -77,8 +68,8 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
- && _libraryManager.IsAudioFile(filename))
+ if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
+ && AudioFileParser.IsAudioFile(filename, _namingOptions))
{
return true;
}
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 6c65b5899..868071a99 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -4,6 +4,7 @@
using System;
using System.Globalization;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
@@ -41,6 +42,11 @@ namespace Emby.Server.Implementations.Library
return _closeFn();
}
+ public Stream GetStream()
+ {
+ throw new NotSupportedException();
+ }
+
public Task Open(CancellationToken openCancellationToken)
{
return Task.CompletedTask;
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 028673529..270264dba 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -11,16 +11,15 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Naming.Audio;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
-using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -78,6 +77,7 @@ namespace Emby.Server.Implementations.Library
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly IImageProcessor _imageProcessor;
+ private readonly NamingOptions _namingOptions;
/// <summary>
/// The _root folder sync lock.
@@ -87,9 +87,6 @@ namespace Emby.Server.Implementations.Library
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
- private NamingOptions _namingOptions;
- private string[] _videoFileExtensions;
-
/// <summary>
/// The _root folder.
/// </summary>
@@ -115,6 +112,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
+ /// <param name="namingOptions">The naming options.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILogger<LibraryManager> logger,
@@ -129,7 +127,8 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
- IMemoryCache memoryCache)
+ IMemoryCache memoryCache,
+ NamingOptions namingOptions)
{
_appHost = appHost;
_logger = logger;
@@ -145,6 +144,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
_memoryCache = memoryCache;
+ _namingOptions = namingOptions;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -286,14 +286,14 @@ namespace Emby.Server.Implementations.Library
if (item is IItemByName)
{
- if (!(item is MusicArtist))
+ if (item is not MusicArtist)
{
return;
}
}
else if (!item.IsFolder)
{
- if (!(item is Video) && !(item is LiveTvChannel))
+ if (item is not Video && item is not LiveTvChannel)
{
return;
}
@@ -332,8 +332,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- var task = BaseItem.ChannelManager.DeleteItem(item);
- Task.WaitAll(task);
+ BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
}
catch (ArgumentException)
{
@@ -491,7 +490,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path);
+ _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
return null;
}
}
@@ -532,8 +531,8 @@ namespace Emby.Server.Implementations.Library
return key.GetMD5();
}
- public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
- => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
+ public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
+ => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
private BaseItem ResolvePath(
FileSystemMetadata fileInfo,
@@ -646,14 +645,14 @@ namespace Emby.Server.Implementations.Library
/// Determines whether a path should be ignored based on its contents - called after the contents have been read.
/// </summary>
/// <param name="args">The args.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
private static bool ShouldResolvePathContents(ItemResolveArgs args)
{
// Ignore any folders containing a file called .ignore
return !args.ContainsFileSystemEntryByName(".ignore");
}
- public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType)
+ public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
{
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
}
@@ -676,7 +675,7 @@ namespace Emby.Server.Implementations.Library
{
var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
- if (result != null && result.Items.Count > 0)
+ if (result?.Items.Count > 0)
{
var items = new List<BaseItem>();
items.AddRange(result.Items);
@@ -798,7 +797,7 @@ namespace Emby.Server.Implementations.Library
{
var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
- _logger.LogDebug("Creating userRootPath at {path}", userRootPath);
+ _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
Directory.CreateDirectory(userRootPath);
var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
@@ -809,7 +808,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId);
+ _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
}
if (tmpItem == null)
@@ -826,7 +825,7 @@ namespace Emby.Server.Implementations.Library
}
_userRootFolder = tmpItem;
- _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder);
+ _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
}
}
}
@@ -865,7 +864,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path);
- if (!(GetItemById(id) is Person item))
+ if (GetItemById(id) is not Person item)
{
item = new Person
{
@@ -964,7 +963,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(MusicArtist) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@@ -1212,7 +1211,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error resolving shortcut file {file}", i);
+ _logger.LogError(ex, "Error resolving shortcut file {File}", i);
return null;
}
})
@@ -1249,10 +1248,8 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
- foreach (var file in files)
+ foreach (ReadOnlySpan<char> file in files)
{
- // TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
- // https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
@@ -1267,7 +1264,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException">id</exception>
+ /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
public BaseItem GetItemById(Guid id)
{
if (id == Guid.Empty)
@@ -1699,7 +1696,7 @@ namespace Emby.Server.Implementations.Library
if (video == null)
{
- _logger.LogError("Intro resolver returned null for {path}.", info.Path);
+ _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
}
else
{
@@ -1718,7 +1715,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error resolving path {path}.", info.Path);
+ _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
}
}
else
@@ -1760,22 +1757,20 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
- public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList)
+ public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy)
{
var isFirst = true;
IOrderedEnumerable<BaseItem> orderedItems = null;
- foreach (var orderBy in orderByList)
+ foreach (var (name, sortOrder) in orderBy)
{
- var comparer = GetComparer(orderBy.Item1, user);
+ var comparer = GetComparer(name, user);
if (comparer == null)
{
continue;
}
- var sortOrder = orderBy.Item2;
-
if (isFirst)
{
orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
@@ -2117,7 +2112,7 @@ namespace Emby.Server.Implementations.Library
public LibraryOptions GetLibraryOptions(BaseItem item)
{
- if (!(item is CollectionFolder collectionFolder))
+ if (item is not CollectionFolder collectionFolder)
{
// List.Find is more performant than FirstOrDefault due to enumerator allocation
collectionFolder = GetCollectionFolders(item)
@@ -2505,16 +2500,6 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public bool IsVideoFile(string path)
- {
- return VideoResolver.IsVideoFile(path, GetNamingOptions());
- }
-
- /// <inheritdoc />
- public bool IsAudioFile(string path)
- => AudioFileParser.IsAudioFile(path, GetNamingOptions());
-
- /// <inheritdoc />
public int? GetSeasonNumberFromPath(string path)
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
@@ -2529,7 +2514,7 @@ namespace Emby.Server.Implementations.Library
isAbsoluteNaming = null;
}
- var resolver = new EpisodeResolver(GetNamingOptions());
+ var resolver = new EpisodeResolver(_namingOptions);
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
@@ -2539,9 +2524,10 @@ namespace Emby.Server.Implementations.Library
{
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
// Resolve from parent folder if it's not the Season folder
- if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
+ var parent = episode.GetParent();
+ if (episodeInfo == null && parent.GetType() == typeof(Folder))
{
- episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
+ episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
if (episodeInfo != null)
{
// add the container
@@ -2685,21 +2671,9 @@ namespace Emby.Server.Implementations.Library
return changed;
}
- /// <inheritdoc />
- public NamingOptions GetNamingOptions()
- {
- if (_namingOptions == null)
- {
- _namingOptions = new NamingOptions();
- _videoFileExtensions = _namingOptions.VideoFileExtensions;
- }
-
- return _namingOptions;
- }
-
public ItemLookupInfo ParseName(string name)
{
- var namingOptions = GetNamingOptions();
+ var namingOptions = _namingOptions;
var result = VideoResolver.CleanDateTime(name, namingOptions);
return new ItemLookupInfo
@@ -2709,89 +2683,105 @@ namespace Emby.Server.Implementations.Library
};
}
- public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ public IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
- var namingOptions = GetNamingOptions();
-
- var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
- .ToList();
-
- var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
-
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
-
- if (currentVideo != null)
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ if (ownerVideoInfo == null)
{
- files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ yield break;
}
- var resolvers = new IItemResolver[]
+ var count = fileSystemChildren.Count;
+ var files = new List<VideoFileInfo>();
+ var nonVideoFiles = new List<FileSystemMetadata>();
+ for (var i = 0; i < count; i++)
{
- new GenericVideoResolver<Trailer>(this)
- };
+ var current = fileSystemChildren[i];
+ if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
+ {
+ var filesInSubFolder = _fileSystem.GetFiles(current.FullName, _namingOptions.VideoFileExtensions, false, false);
+ foreach (var file in filesInSubFolder)
+ {
+ var videoInfo = VideoResolver.Resolve(file.FullName, file.IsDirectory, _namingOptions);
+ if (videoInfo == null)
+ {
+ nonVideoFiles.Add(file);
+ continue;
+ }
- return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
- .OfType<Trailer>()
- .Select(video =>
+ files.Add(videoInfo);
+ }
+ }
+ else if (!current.IsDirectory)
{
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- if (GetItemById(video.Id) is Trailer dbItem)
+ var videoInfo = VideoResolver.Resolve(current.FullName, current.IsDirectory, _namingOptions);
+ if (videoInfo == null)
{
- video = dbItem;
+ nonVideoFiles.Add(current);
+ continue;
}
- video.ParentId = Guid.Empty;
- video.OwnerId = owner.Id;
- video.ExtraType = ExtraType.Trailer;
- video.TrailerTypes = new[] { TrailerType.LocalTrailer };
-
- return video;
-
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path);
- }
-
- public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
- {
- var namingOptions = GetNamingOptions();
+ files.Add(videoInfo);
+ }
+ }
- var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
- .ToList();
+ if (files.Count == 0)
+ {
+ yield break;
+ }
- var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
+ var videos = VideoListResolver.Resolve(files, _namingOptions);
+ // owner video info cannot be null as that implies it has no path
+ var extras = ExtraResolver.GetExtras(videos, ownerVideoInfo, _namingOptions.VideoFlagDelimiters);
+ for (var i = 0; i < extras.Count; i++)
+ {
+ var currentExtra = extras[i];
+ var resolved = ResolvePath(_fileSystem.GetFileInfo(currentExtra.Path), null, directoryService);
+ if (resolved is not Video video)
+ {
+ continue;
+ }
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ if (GetItemById(resolved.Id) is Video dbItem)
+ {
+ video = dbItem;
+ }
- if (currentVideo != null)
- {
- files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
+ video.ExtraType = currentExtra.ExtraType;
+ video.ParentId = Guid.Empty;
+ video.OwnerId = owner.Id;
+ yield return video;
}
- return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
- .OfType<Video>()
- .Select(video =>
+ // TODO: theme songs must be handled "manually" (but should we?) since they aren't video files
+ for (var i = 0; i < nonVideoFiles.Count; i++)
+ {
+ var current = nonVideoFiles[i];
+ var extraInfo = ExtraResolver.GetExtraInfo(current.FullName, _namingOptions);
+ if (extraInfo.ExtraType != ExtraType.ThemeSong)
{
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- var dbItem = GetItemById(video.Id) as Video;
-
- if (dbItem != null)
- {
- video = dbItem;
- }
+ continue;
+ }
- video.ParentId = Guid.Empty;
- video.OwnerId = owner.Id;
+ var resolved = ResolvePath(current, null, directoryService);
+ if (resolved is not Audio themeSong)
+ {
+ continue;
+ }
- SetExtraTypeFromFilename(video);
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ if (GetItemById(themeSong.Id) is Audio dbItem)
+ {
+ themeSong = dbItem;
+ }
- return video;
+ themeSong.ExtraType = ExtraType.ThemeSong;
+ themeSong.OwnerId = owner.Id;
+ themeSong.ParentId = Guid.Empty;
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path);
+ yield return themeSong;
+ }
}
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
@@ -2841,15 +2831,6 @@ namespace Emby.Server.Implementations.Library
return path;
}
- private void SetExtraTypeFromFilename(Video item)
- {
- var resolver = new ExtraResolver(GetNamingOptions());
-
- var result = resolver.GetExtraInfo(item.Path);
-
- item.ExtraType = result.ExtraType;
- }
-
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{
return _itemRepository.GetPeople(query);
@@ -2956,11 +2937,12 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var existingNameCount = 1; // first numbered name will be 2
var virtualFolderPath = Path.Combine(rootFolderPath, name);
while (Directory.Exists(virtualFolderPath))
{
- name += "1";
- virtualFolderPath = Path.Combine(rootFolderPath, name);
+ existingNameCount++;
+ virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
}
var mediaPathInfos = options.PathInfos;
@@ -3074,9 +3056,9 @@ namespace Emby.Server.Implementations.Library
});
}
- public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
+ public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
{
- AddMediaPathInternal(virtualFolderName, pathInfo, true);
+ AddMediaPathInternal(virtualFolderName, mediaPath, true);
}
private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
@@ -3129,11 +3111,11 @@ namespace Emby.Server.Implementations.Library
}
}
- public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
+ public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
{
- if (pathInfo == null)
+ if (mediaPath == null)
{
- throw new ArgumentNullException(nameof(pathInfo));
+ throw new ArgumentNullException(nameof(mediaPath));
}
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
@@ -3146,9 +3128,9 @@ namespace Emby.Server.Implementations.Library
var list = libraryOptions.PathInfos.ToList();
foreach (var originalPathInfo in list)
{
- if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal))
+ if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
{
- originalPathInfo.NetworkPath = pathInfo.NetworkPath;
+ originalPathInfo.NetworkPath = mediaPath.NetworkPath;
break;
}
}
@@ -3171,10 +3153,7 @@ namespace Emby.Server.Implementations.Library
{
if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
{
- list.Add(new MediaPathInfo
- {
- Path = location
- });
+ list.Add(new MediaPathInfo(location));
}
}
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 4ef7923db..83acd8e9f 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -10,13 +10,14 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = File.OpenWrite(cacheFilePath);
+ await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index b812b6b61..972d4ebbb 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -13,9 +13,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
+ private readonly IDirectoryService _directoryService;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
@@ -61,7 +62,8 @@ namespace Emby.Server.Implementations.Library
ILogger<MediaSourceManager> logger,
IFileSystem fileSystem,
IUserDataManager userDataManager,
- IMediaEncoder mediaEncoder)
+ IMediaEncoder mediaEncoder,
+ IDirectoryService directoryService)
{
_itemRepo = itemRepo;
_userManager = userManager;
@@ -72,6 +74,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_localizationManager = localizationManager;
_appPaths = applicationPaths;
+ _directoryService = directoryService;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -106,16 +109,6 @@ namespace Emby.Server.Implementations.Library
return false;
}
- public List<MediaStream> GetMediaStreams(string mediaSourceId)
- {
- var list = GetMediaStreams(new MediaStreamQuery
- {
- ItemId = new Guid(mediaSourceId)
- });
-
- return GetMediaStreamsForItem(list);
- }
-
public List<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
@@ -161,7 +154,7 @@ namespace Emby.Server.Implementations.Library
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
{
await item.RefreshMetadata(
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ new MetadataRefreshOptions(_directoryService)
{
EnableRemoteContentProbe = true,
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
@@ -212,6 +205,7 @@ namespace Emby.Server.Implementations.Library
return SortMediaSources(list);
}
+ /// <inheritdoc />>
public MediaProtocol GetPathProtocol(string path)
{
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
@@ -258,7 +252,7 @@ namespace Emby.Server.Implementations.Library
{
if (path != null)
{
- if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1)
+ if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -297,7 +291,7 @@ namespace Emby.Server.Implementations.Library
catch (Exception ex)
{
_logger.LogError(ex, "Error getting media sources");
- return new List<MediaSourceInfo>();
+ return Enumerable.Empty<MediaSourceInfo>();
}
}
@@ -494,14 +488,11 @@ namespace Emby.Server.Implementations.Library
_liveStreamSemaphore.Release();
}
- // TODO: Don't hardcode this
- const bool isAudio = false;
-
try
{
if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
{
- AddMediaInfo(mediaSource, isAudio);
+ AddMediaInfo(mediaSource);
}
else
{
@@ -509,19 +500,19 @@ namespace Emby.Server.Implementations.Library
string cacheKey = request.OpenToken;
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
- .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
+ .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken)
.ConfigureAwait(false);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error probing live tv stream");
- AddMediaInfo(mediaSource, isAudio);
+ AddMediaInfo(mediaSource);
}
// TODO: @bond Fix
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
- _logger.LogInformation("Live stream opened: " + json);
+ _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
@@ -536,7 +527,7 @@ namespace Emby.Server.Implementations.Library
return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
}
- private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)
+ private static void AddMediaInfo(MediaSourceInfo mediaSource)
{
mediaSource.DefaultSubtitleStreamIndex = null;
@@ -587,13 +578,6 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
- {
- var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
-
- return Task.FromResult(info.Value as IDirectStreamProvider);
- }
-
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
{
var result = await OpenLiveStreamInternal(request, cancellationToken).ConfigureAwait(false);
@@ -602,7 +586,8 @@ namespace Emby.Server.Implementations.Library
public async Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken)
{
- var liveStreamInfo = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var liveStreamInfo = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
var mediaSource = liveStreamInfo.MediaSource;
@@ -638,7 +623,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- await using FileStream jsonStream = File.OpenRead(cacheFilePath);
+ await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
@@ -771,18 +756,19 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate(true);
}
- public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
+ public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- var info = await GetLiveStreamInfo(id, cancellationToken).ConfigureAwait(false);
- return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
+ // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
+ var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
+ return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
- private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ public ILiveStream GetLiveStreamInfo(string id)
{
if (string.IsNullOrEmpty(id))
{
@@ -791,12 +777,16 @@ namespace Emby.Server.Implementations.Library
if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- return Task.FromResult(info);
- }
- else
- {
- return Task.FromException<ILiveStream>(new ResourceNotFoundException());
+ return info;
}
+
+ return null;
+ }
+
+ /// <inheritdoc />
+ public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId)
+ {
+ return _openStreams.Values.FirstOrDefault(stream => string.Equals(uniqueId, stream?.UniqueId, StringComparison.OrdinalIgnoreCase));
}
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
@@ -856,9 +846,7 @@ namespace Emby.Server.Implementations.Library
return (provider, keyId);
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
+ /// <inheritdoc />
public void Dispose()
{
Dispose(true);
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index b833122ea..da0c89c13 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library
@@ -38,14 +39,11 @@ namespace Emby.Server.Implementations.Library
}
public static int? GetDefaultSubtitleStreamIndex(
- List<MediaStream> streams,
+ IEnumerable<MediaStream> streams,
string[] preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
{
- streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages)
- .ToList();
-
MediaStream stream = null;
if (mode == SubtitlePlaybackMode.None)
@@ -53,52 +51,48 @@ namespace Emby.Server.Implementations.Library
return null;
}
+ var sortedStreams = streams
+ .Where(i => i.Type == MediaStreamType.Subtitle)
+ .OrderByDescending(x => x.IsExternal)
+ .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
+ .ThenByDescending(x => x.IsForced)
+ .ThenByDescending(x => x.IsDefault)
+ .ToList();
+
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
-
- stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ??
- streams.FirstOrDefault(s => s.IsForced) ??
- streams.FirstOrDefault(s => s.IsDefault);
+ stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault);
// if the audio language is not understood by the user, load their preferred subs, if there are any
- if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+ if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+ stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
}
}
else if (mode == SubtitlePlaybackMode.Smart)
{
- // Prefer smart logic over embedded metadata
-
// if the audio language is not understood by the user, load their preferred subs, if there are any
- if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+ if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- stream = streams.Where(s => !s.IsForced).FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ??
- streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+ stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) ??
+ streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
// always load the most suitable full subtitles
- stream = streams.FirstOrDefault(s => !s.IsForced);
+ stream = sortedStreams.FirstOrDefault(s => !s.IsForced);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
// always load the most suitable full subtitles
- stream = streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) ??
- streams.FirstOrDefault(s => s.IsForced);
+ stream = sortedStreams.FirstOrDefault(x => x.IsForced);
}
// load forced subs if we have found no suitable full subtitles
- stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
-
- if (stream != null)
- {
- return stream.Index;
- }
-
- return null;
+ stream ??= sortedStreams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+ return stream?.Index;
}
private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences)
@@ -143,9 +137,9 @@ namespace Emby.Server.Implementations.Library
else if (mode == SubtitlePlaybackMode.Smart)
{
// Prefer smart logic over embedded metadata
- if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+ if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
- filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase))
+ filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 06300adeb..d33213564 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -36,9 +36,10 @@ namespace Emby.Server.Implementations.Library
return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList();
}
- public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions)
+ /// <inheritdoc />
+ public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions)
{
- return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
+ return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions)
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Library
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(Audio) },
+ IncludeItemTypes = new[] { BaseItemKind.Audio },
DtoOptions = dtoOptions
})
.Cast<Audio>()
@@ -88,7 +89,7 @@ namespace Emby.Server.Implementations.Library
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(Audio) },
+ IncludeItemTypes = new[] { BaseItemKind.Audio },
GenreIds = genreIds.ToArray(),
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 86b8039fa..6f61dc713 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="attribute">The attrib.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
- public static string? GetAttributeValue(this string str, string attribute)
+ public static string? GetAttributeValue(this ReadOnlySpan<char> str, ReadOnlySpan<char> attribute)
{
if (str.Length == 0)
{
@@ -28,17 +28,31 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("String can't be empty.", nameof(attribute));
}
- string srch = "[" + attribute + "=";
- int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
- if (start != -1)
+ var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
+
+ // Must be at least 3 characters after the attribute =, ], any character.
+ var maxIndex = str.Length - attribute.Length - 3;
+ while (attributeIndex > -1 && attributeIndex < maxIndex)
{
- start += srch.Length;
- int end = str.IndexOf(']', start);
- return str.Substring(start, end - start);
+ var attributeEnd = attributeIndex + attribute.Length;
+ if (attributeIndex > 0
+ && str[attributeIndex - 1] == '['
+ && str[attributeEnd] == '=')
+ {
+ var closingIndex = str[attributeEnd..].IndexOf(']');
+ // Must be at least 1 character before the closing bracket.
+ if (closingIndex > 1)
+ {
+ return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+ }
+ }
+
+ str = str[attributeEnd..];
+ attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
}
// for imdbid we also accept pattern matching
- if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
+ if (attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase))
{
var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
return match ? imdbId.ToString() : null;
@@ -53,7 +67,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param>
- /// <param name="newPath">The result of the sub path replacement</param>
+ /// <param name="newPath">The result of the sub path replacement.</param>
/// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
public static bool TryReplaceSubPath(
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index e893d6335..7a6aea9c1 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -6,7 +6,10 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Emby.Naming.Audio;
using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
{
- private readonly ILibraryManager LibraryManager;
+ private readonly NamingOptions _namingOptions;
- public AudioResolver(ILibraryManager libraryManager)
+ public AudioResolver(NamingOptions namingOptions)
{
- LibraryManager = libraryManager;
+ _namingOptions = namingOptions;
}
/// <summary>
@@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
string collectionType,
IDirectoryService directoryService)
{
- var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+ var result = ResolveMultipleInternal(parent, files, collectionType);
if (result != null)
{
@@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType,
- IDirectoryService directoryService)
+ string collectionType)
{
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
{
- return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true);
+ return ResolveMultipleAudio(parent, files, true);
}
return null;
@@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
- var files = args.FileSystemChildren
- .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
- .ToList();
-
- return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ return FindAudioBook(args, false);
}
- if (LibraryManager.IsAudioFile(args.Path))
+ if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{
var extension = Path.GetExtension(args.Path);
@@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
// For conflicting extensions, give priority to videos
- if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path))
+ if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
{
return null;
}
@@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null;
}
- private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
- where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
+ private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
{
// TODO: Allow GetMultiDiscMovie in here
- const bool supportsMultiVersion = false;
+ var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName);
- var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ??
- new MultiItemResolverResult();
-
- if (result.Items.Count == 1)
+ if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
{
- // If we were supporting this we'd be checking filesFromOtherItems
- var item = (T)result.Items[0];
- item.IsInMixedFolder = false;
- item.Name = Path.GetFileName(item.ContainingFolderPath);
- return item;
+ return null;
}
- return null;
+ // If we were supporting this we'd be checking filesFromOtherItems
+ item.IsInMixedFolder = false;
+ item.Name = Path.GetFileName(item.ContainingFolderPath);
+ return item;
}
- private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName)
- where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
+ private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
{
var files = new List<FileSystemMetadata>();
var items = new List<BaseItem>();
@@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
leftOver.Add(child);
}
- else if (!IsIgnored(child.Name))
+ else
{
files.Add(child);
}
}
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
- var resolver = new AudioBookListResolver(namingOptions);
+ var resolver = new AudioBookListResolver(_namingOptions);
var resolverResult = resolver.Resolve(files).ToList();
var result = new MultiItemResolverResult
@@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var firstMedia = resolvedItem.Files[0];
- var libraryItem = new T
+ var libraryItem = new AudioBook
{
Path = firstMedia.Path,
IsInMixedFolder = isInMixedFolder,
@@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result;
}
- private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file)
+ private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file)
{
return result.Any(i => ContainsFile(i, file));
}
- private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
+ private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
{
return result.Files.Any(i => ContainsFile(i, file)) ||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
@@ -246,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
}
-
- private static bool IsIgnored(string filename)
- {
- return false;
- }
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 8e1eccb10..a9819a364 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
{
private readonly ILogger<MusicAlbumResolver> _logger;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryManager _libraryManager;
+ private readonly NamingOptions _namingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="libraryManager">The library manager.</param>
- public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+ /// <param name="namingOptions">The naming options.</param>
+ public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
{
_logger = logger;
- _fileSystem = fileSystem;
- _libraryManager = libraryManager;
+ _namingOptions = namingOptions;
}
/// <summary>
@@ -82,9 +80,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Determine if the supplied file data points to a music album.
/// </summary>
+ /// <param name="path">The path to check.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{
- return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
+ return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
}
/// <summary>
@@ -98,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (args.IsDirectory)
{
// if (args.Parent is MusicArtist) return true; // saves us from testing children twice
- if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
+ if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
{
return true;
}
@@ -113,13 +114,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private bool ContainsMusic(
IEnumerable<FileSystemMetadata> list,
bool allowSubfolders,
- IDirectoryService directoryService,
- ILogger<MusicAlbumResolver> logger,
- IFileSystem fileSystem,
- ILibraryManager libraryManager)
+ IDirectoryService directoryService)
{
// check for audio files before digging down into directories
- var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+ var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
if (foundAudioFile)
{
// at least one audio file exists
@@ -134,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var discSubfolderCount = 0;
- var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
- var parser = new AlbumParser(namingOptions);
+ var parser = new AlbumParser(_namingOptions);
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
var path = fileSystemInfo.FullName;
- var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService);
if (hasMusic)
{
if (parser.IsMultiPart(path))
{
- logger.LogDebug("Found multi-disc folder: " + path);
+ _logger.LogDebug("Found multi-disc folder: {Path}", path);
Interlocked.Increment(ref discSubfolderCount);
}
else
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 3d2ae95d2..210ed0953 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -3,12 +3,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Resolvers.Audio
@@ -19,27 +18,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
private readonly ILogger<MusicAlbumResolver> _logger;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryManager _libraryManager;
- private readonly IServerConfigurationManager _config;
+ private NamingOptions _namingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary>
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
- /// <param name="fileSystem">The file system.</param>
- /// <param name="libraryManager">The library manager.</param>
- /// <param name="config">The configuration manager.</param>
+ /// <param name="namingOptions">The naming options.</param>
public MusicArtistResolver(
ILogger<MusicAlbumResolver> logger,
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- IServerConfigurationManager config)
+ NamingOptions namingOptions)
{
_logger = logger;
- _fileSystem = fileSystem;
- _libraryManager = libraryManager;
- _config = config;
+ _namingOptions = namingOptions;
}
/// <summary>
@@ -89,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var directoryService = args.DirectoryService;
- var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
+ var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
// If we contain an album assume we are an artist folder
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index cdb492022..9222a9479 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -5,6 +5,8 @@
using System;
using System.IO;
using System.Linq;
+using DiscUtils.Udf;
+using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -16,17 +18,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Resolves a Path into a Video or Video subclass.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of item to resolve.</typeparam>
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
where T : Video, new()
{
- protected readonly ILibraryManager LibraryManager;
-
- protected BaseVideoResolver(ILibraryManager libraryManager)
+ protected BaseVideoResolver(NamingOptions namingOptions)
{
- LibraryManager = libraryManager;
+ NamingOptions = namingOptions;
}
+ protected NamingOptions NamingOptions { get; }
+
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -47,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
- var namingOptions = LibraryManager.GetNamingOptions();
+ VideoFileInfo videoInfo = null;
+ VideoType? videoType = null;
// If the path is a file check for a matching extensions
if (args.IsDirectory)
{
- TVideoType video = null;
- VideoFileInfo videoInfo = null;
-
// Loop through each child file/folder and see if we find a video
foreach (var child in args.FileSystemChildren)
{
var filename = child.Name;
-
if (child.IsDirectory)
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
- videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- video = new TVideoType
- {
- Path = args.Path,
- VideoType = VideoType.Dvd,
- ProductionYear = videoInfo.Year
- };
- break;
+ videoType = VideoType.Dvd;
}
-
- if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
+ else if (IsBluRayDirectory(filename))
{
- videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- video = new TVideoType
- {
- Path = args.Path,
- VideoType = VideoType.BluRay,
- ProductionYear = videoInfo.Year
- };
- break;
+ videoType = VideoType.BluRay;
}
}
else if (IsDvdFile(filename))
{
- videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- video = new TVideoType
- {
- Path = args.Path,
- VideoType = VideoType.Dvd,
- ProductionYear = videoInfo.Year
- };
- break;
+ videoType = VideoType.Dvd;
}
- }
- if (video != null)
- {
- video.Name = parseName ?
- videoInfo.Name :
- Path.GetFileName(args.Path);
+ if (videoType == null)
+ {
+ continue;
+ }
- Set3DFormat(video, videoInfo);
+ videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName);
+ break;
}
-
- return video;
}
else
{
- var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
-
- if (videoInfo == null)
- {
- return null;
- }
-
- if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
- {
- var path = args.Path;
-
- var video = new TVideoType
- {
- Path = path,
- IsInMixedFolder = true,
- ProductionYear = videoInfo.Year
- };
-
- SetVideoType(video, videoInfo);
+ videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
+ }
- video.Name = parseName ?
- videoInfo.Name :
- Path.GetFileNameWithoutExtension(args.Path);
+ if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions)))
+ {
+ return null;
+ }
- Set3DFormat(video, videoInfo);
+ var video = new TVideoType
+ {
+ Name = videoInfo.Name,
+ Path = args.Path,
+ ProductionYear = videoInfo.Year,
+ ExtraType = videoInfo.ExtraType
+ };
- return video;
- }
+ if (videoType.HasValue)
+ {
+ video.VideoType = videoType.Value;
+ }
+ else
+ {
+ SetVideoType(video, videoInfo);
}
- return null;
+ Set3DFormat(video, videoInfo);
+
+ return video;
}
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
@@ -201,6 +154,22 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
video.IsoType = IsoType.BluRay;
}
+ else
+ {
+ // use disc-utils, both DVDs and BDs use UDF filesystem
+ using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
+ using (UdfReader udfReader = new UdfReader(videoFileStream))
+ {
+ if (udfReader.DirectoryExists("VIDEO_TS"))
+ {
+ video.IsoType = IsoType.Dvd;
+ }
+ else if (udfReader.DirectoryExists("BDMV"))
+ {
+ video.IsoType = IsoType.BluRay;
+ }
+ }
+ }
}
}
@@ -250,7 +219,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void Set3DFormat(Video video)
{
- var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
+ var result = Format3DParser.Parse(video.Path, NamingOptions);
Set3DFormat(video, result.Is3D, result.Format3D);
}
@@ -258,6 +227,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Determines whether [is DVD directory] [the specified directory name].
/// </summary>
+ /// <param name="fullPath">The full path of the directory.</param>
+ /// <param name="directoryName">The name of the directory.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns><c>true</c> if the provided directory is a DVD directory, <c>false</c> otherwise.</returns>
protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
{
if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase))
@@ -279,25 +252,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
/// <summary>
- /// Determines whether [is blu ray directory] [the specified directory name].
+ /// Determines whether [is bluray directory] [the specified directory name].
/// </summary>
- protected bool IsBluRayDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
+ /// <param name="directoryName">The directory name.</param>
+ /// <returns>Whether the directory is a bluray directory.</returns>
+ protected bool IsBluRayDirectory(string directoryName)
{
- if (!string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
-
- return true;
- // var blurayExtensions = new[]
- //{
- // ".mts",
- // ".m2ts",
- // ".bdmv",
- // ".mpls"
- //};
-
- // return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase));
+ return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 68076730b..8f224f547 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -32,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var extension = Path.GetExtension(args.Path);
- if (extension != null && _validExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (extension != null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's a book
return new Book
@@ -49,13 +50,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
- var fileExtension = Path.GetExtension(f.FullName) ??
- string.Empty;
+ var fileExtension = Path.GetExtension(f.FullName)
+ ?? string.Empty;
return _validExtensions.Contains(
fileExtension,
- StringComparer
- .OrdinalIgnoreCase);
+ StringComparer.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory
diff --git a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 7aaee017d..db7703cd6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class FolderResolver.
/// </summary>
- public class FolderResolver : FolderResolver<Folder>
+ public class FolderResolver : GenericFolderResolver<Folder>
{
/// <summary>
/// Gets the priority.
@@ -32,24 +32,4 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
}
-
- /// <summary>
- /// Class FolderResolver.
- /// </summary>
- /// <typeparam name="TItemType">The type of the T item type.</typeparam>
- public abstract class FolderResolver<TItemType> : ItemResolver<TItemType>
- where TItemType : Folder, new()
- {
- /// <summary>
- /// Sets the initial item values.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="args">The args.</param>
- protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
- {
- base.SetInitialItemValues(item, args);
-
- item.IsRoot = args.Parent == null;
- }
- }
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
new file mode 100644
index 000000000..f109a5e9a
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericFolderResolver.cs
@@ -0,0 +1,27 @@
+#nullable disable
+
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+
+namespace Emby.Server.Implementations.Library.Resolvers
+{
+ /// <summary>
+ /// Class FolderResolver.
+ /// </summary>
+ /// <typeparam name="TItemType">The type of the T item type.</typeparam>
+ public abstract class GenericFolderResolver<TItemType> : ItemResolver<TItemType>
+ where TItemType : Folder, new()
+ {
+ /// <summary>
+ /// Sets the initial item values.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
+ {
+ base.SetInitialItemValues(item, args);
+
+ item.IsRoot = args.Parent == null;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
index 9599faea4..72341d9db 100644
--- a/Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
@@ -2,16 +2,16 @@
#pragma warning disable CS1591
+using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.Resolvers
{
public class GenericVideoResolver<T> : BaseVideoResolver<T>
where T : Video, new()
{
- public GenericVideoResolver(ILibraryManager libraryManager)
- : base(libraryManager)
+ public GenericVideoResolver(NamingOptions namingOptions)
+ : base(namingOptions)
{
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
index fa45ccf84..3f29ab191 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class ItemResolver.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of BaseItem.</typeparam>
public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new()
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 69d71d0d9..6cc04ea81 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary>
/// Class BoxSetResolver.
/// </summary>
- public class BoxSetResolver : FolderResolver<BoxSet>
+ public class BoxSetResolver : GenericFolderResolver<BoxSet>
{
/// <summary>
/// Resolves the specified args.
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static void SetProviderIdFromPath(BaseItem item)
{
// we need to only look at the name of this actual item (not parents)
- var justName = Path.GetFileName(item.Path);
+ var justName = Path.GetFileName(item.Path.AsSpan());
var id = justName.GetAttributeValue("tmdbid");
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 97f96f746..4feaf3fb4 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -5,8 +5,9 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using Emby.Naming.Common;
using Emby.Naming.Video;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -24,6 +25,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// </summary>
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
+ private readonly IImageProcessor _imageProcessor;
+
private string[] _validCollectionTypes = new[]
{
CollectionType.Movies,
@@ -33,15 +36,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
CollectionType.Photos
};
- private readonly IImageProcessor _imageProcessor;
-
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary>
- /// <param name="libraryManager">The library manager.</param>
/// <param name="imageProcessor">The image processor.</param>
- public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
- : base(libraryManager)
+ /// <param name="namingOptions">The naming options.</param>
+ public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
+ : base(namingOptions)
{
_imageProcessor = imageProcessor;
}
@@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
string collectionType,
IDirectoryService directoryService)
{
- var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+ var result = ResolveMultipleInternal(parent, files, collectionType);
if (result != null)
{
@@ -89,18 +90,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- var files = args.FileSystemChildren
- .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
- .ToList();
+ Video movie = null;
+ var files = args.GetActualFileSystemChildren().ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
- return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
- return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+ movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@@ -117,17 +117,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- {
- return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
- }
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
- return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+ movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
- return null;
+ // ignore extras
+ return movie?.ExtraType == null ? movie : null;
}
// Handle owned items
@@ -168,6 +167,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
item = ResolveVideo<Video>(args, false);
}
+ // Ignore extras
+ if (item?.ExtraType != null)
+ {
+ return null;
+ }
+
if (item != null)
{
item.IsInMixedFolder = true;
@@ -179,8 +184,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
- string collectionType,
- IDirectoryService directoryService)
+ string collectionType)
{
if (IsInvalid(parent, collectionType))
{
@@ -189,13 +193,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
- return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
+ return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
- return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
+ return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
@@ -203,7 +207,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// Owned items should just use the plain video type
if (parent == null)
{
- return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
+ return ResolveVideos<Video>(parent, files, false, collectionType, false);
}
if (parent is Series || parent.GetParents().OfType<Series>().Any())
@@ -211,12 +215,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
+ return ResolveVideos<Movie>(parent, files, false, collectionType, true);
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
- return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
+ return ResolveVideos<Movie>(parent, files, true, collectionType, true);
}
return null;
@@ -225,21 +229,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private MultiItemResolverResult ResolveVideos<T>(
Folder parent,
IEnumerable<FileSystemMetadata> fileSystemEntries,
- IDirectoryService directoryService,
- bool suppportMultiEditions,
+ bool supportMultiEditions,
string collectionType,
bool parseName)
where T : Video, new()
{
var files = new List<FileSystemMetadata>();
- var videos = new List<BaseItem>();
var leftOver = new List<FileSystemMetadata>();
+ var hasCollectionType = !string.IsNullOrEmpty(collectionType);
// Loop through each child file/folder and see if we find a video
foreach (var child in fileSystemEntries)
{
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
- if (string.IsNullOrEmpty(collectionType))
+ if (!hasCollectionType)
{
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
@@ -258,31 +261,39 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- var namingOptions = LibraryManager.GetNamingOptions();
+ var videoInfos = files
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+ .Where(f => f != null)
+ .ToList();
- var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
+ var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
var result = new MultiItemResolverResult
{
- ExtraFiles = leftOver,
- Items = videos
+ ExtraFiles = leftOver
};
- var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
+ var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
foreach (var video in resolverResult)
{
var firstVideo = video.Files[0];
+ var path = firstVideo.Path;
+ if (video.ExtraType != null)
+ {
+ result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase)));
+ continue;
+ }
+
+ var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>();
var videoItem = new T
{
- Path = video.Files[0].Path,
+ Path = path,
IsInMixedFolder = isInMixedFolder,
ProductionYear = video.Year,
- Name = parseName ?
- video.Name :
- Path.GetFileNameWithoutExtension(video.Files[0].Path),
- AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
+ Name = parseName ? video.Name : firstVideo.Name,
+ AdditionalParts = additionalParts,
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
};
@@ -300,21 +311,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static bool IsIgnored(string filename)
{
// Ignore samples
- Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
+ Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
return m.Success;
}
- private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
+ private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{
- return result.Any(i => ContainsFile(i, file));
- }
+ for (var i = 0; i < result.Count; i++)
+ {
+ var current = result[i];
+ for (var j = 0; j < current.Files.Count; j++)
+ {
+ if (ContainsFile(current.Files[j], file))
+ {
+ return true;
+ }
+ }
- private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
- {
- return result.Files.Any(i => ContainsFile(i, file)) ||
- result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
- result.Extras.Any(i => ContainsFile(i, file));
+ for (var j = 0; j < current.AlternateVersions.Count; j++)
+ {
+ if (ContainsFile(current.AlternateVersions[j], file))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
@@ -343,9 +367,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (item is Movie || item is MusicVideo)
{
// We need to only look at the name of this actual item (not parents)
- var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath);
+ var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
- if (!string.IsNullOrEmpty(justName))
+ if (!justName.IsEmpty)
{
// check for tmdb id
var tmdbid = justName.GetAttributeValue("tmdbid");
@@ -359,7 +383,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrEmpty(item.Path))
{
// check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
- var imdbid = item.Path.GetAttributeValue("imdbid");
+ var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
if (!string.IsNullOrWhiteSpace(imdbid))
{
@@ -400,7 +424,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return movie;
}
- if (IsBluRayDirectory(child.FullName, filename, directoryService))
+ if (IsBluRayDirectory(filename))
{
var movie = new T
{
@@ -432,13 +456,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// TODO: Allow GetMultiDiscMovie in here
const bool SupportsMultiVersion = true;
- var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ??
+ var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
if (result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;
- var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name));
+ var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
if (!hasPhotos)
{
@@ -481,7 +505,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return true;
}
- if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService)))
+ if (subfolders.Any(s => IsBluRayDirectory(s.Name)))
{
videoTypes.Add(VideoType.BluRay);
return true;
@@ -511,9 +535,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null;
}
- var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
- var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
+ var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
if (result.Count != 1)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
index 534bc80dd..7dd0ab185 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -1,6 +1,7 @@
#nullable disable
using System;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -12,20 +13,20 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Class PhotoAlbumResolver.
/// </summary>
- public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
+ public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
{
private readonly IImageProcessor _imageProcessor;
- private readonly ILibraryManager _libraryManager;
+ private readonly NamingOptions _namingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
/// </summary>
/// <param name="imageProcessor">The image processor.</param>
- /// <param name="libraryManager">The library manager.</param>
- public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
+ /// <param name="namingOptions">The naming options.</param>
+ public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
{
_imageProcessor = imageProcessor;
- _libraryManager = libraryManager;
+ _namingOptions = namingOptions;
}
/// <inheritdoc />
@@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
foreach (var siblingFile in files)
{
- if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
+ if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
{
ownedByMedia = true;
break;
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index 57bf40e9e..e52b43050 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -6,6 +6,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -16,7 +19,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class PhotoResolver : ItemResolver<Photo>
{
private readonly IImageProcessor _imageProcessor;
- private readonly ILibraryManager _libraryManager;
+ private readonly NamingOptions _namingOptions;
+
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"folder",
@@ -30,10 +34,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
"default"
};
- public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
+
+ public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
{
_imageProcessor = imageProcessor;
- _libraryManager = libraryManager;
+ _namingOptions = namingOptions;
}
/// <summary>
@@ -60,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
foreach (var file in files)
{
- if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
+ if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
{
return null;
}
@@ -77,17 +82,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename)
+ internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
{
- if (libraryManager.IsVideoFile(file))
- {
- return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
- }
-
- return false;
+ return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
- internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
+ internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
@@ -110,7 +110,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
string extension = Path.GetExtension(path).TrimStart('.');
- return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index ecd44be47..6b0dfe986 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Resolvers;
@@ -16,9 +17,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
/// </summary>
- public class PlaylistResolver : FolderResolver<Playlist>
+ public class PlaylistResolver : GenericFolderResolver<Playlist>
{
- private string[] _musicPlaylistCollectionTypes = new string[] {
+ private string[] _musicPlaylistCollectionTypes =
+ {
string.Empty,
CollectionType.Music
};
@@ -56,10 +58,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Check if this is a music playlist file
// It should have the correct collection type and a supported file extension
- else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
var extension = Path.GetExtension(args.Path);
- if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return new Playlist
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 7b4e14334..6bb999641 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -13,7 +13,7 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers
{
- public class SpecialFolderResolver : FolderResolver<Folder>
+ public class SpecialFolderResolver : GenericFolderResolver<Folder>
{
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
@@ -67,7 +67,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return args.FileSystemChildren
.Where(i =>
{
-
try
{
return !i.IsDirectory &&
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index d6ae91056..be9905647 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -2,7 +2,7 @@
using System;
using System.Linq;
-using MediaBrowser.Controller.Entities;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -17,9 +17,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
- /// <param name="libraryManager">The library manager.</param>
- public EpisodeResolver(ILibraryManager libraryManager)
- : base(libraryManager)
+ /// <param name="namingOptions">The naming options.</param>
+ public EpisodeResolver(NamingOptions namingOptions)
+ : base(namingOptions)
{
}
@@ -44,34 +44,36 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
- if ((season != null ||
- string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
- args.HasParent<Series>())
- && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
+ if (season != null ||
+ string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+ args.HasParent<Series>())
{
var episode = ResolveVideo<Episode>(args, false);
- if (episode != null)
+ // Ignore extras
+ if (episode == null || episode.ExtraType != null)
{
- var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
+ return null;
+ }
- if (series != null)
- {
- episode.SeriesId = series.Id;
- episode.SeriesName = series.Name;
- }
+ var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
- if (season != null)
- {
- episode.SeasonId = season.Id;
- episode.SeasonName = season.Name;
- }
+ if (series != null)
+ {
+ episode.SeriesId = series.Id;
+ episode.SeriesName = series.Name;
+ }
- // Assume season 1 if there's no season folder and a season number could not be determined
- if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
- {
- episode.ParentIndexNumber = 1;
- }
+ if (season != null)
+ {
+ episode.SeasonId = season.Id;
+ episode.SeasonName = season.Name;
+ }
+
+ // Assume season 1 if there's no season folder and a season number could not be determined
+ if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
+ {
+ episode.ParentIndexNumber = 1;
}
return episode;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 7d707df18..ea4851458 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -1,6 +1,7 @@
#nullable disable
using System.Globalization;
+using Emby.Naming.Common;
using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -12,24 +13,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Class SeasonResolver.
/// </summary>
- public class SeasonResolver : FolderResolver<Season>
+ public class SeasonResolver : GenericFolderResolver<Season>
{
- private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
private readonly ILogger<SeasonResolver> _logger;
+ private readonly NamingOptions _namingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
/// </summary>
- /// <param name="libraryManager">The library manager.</param>
+ /// <param name="namingOptions">The naming options.</param>
/// <param name="localization">The localization.</param>
/// <param name="logger">The logger.</param>
public SeasonResolver(
- ILibraryManager libraryManager,
+ NamingOptions namingOptions,
ILocalizationManager localization,
ILogger<SeasonResolver> logger)
{
- _libraryManager = libraryManager;
+ _namingOptions = namingOptions;
_localization = localization;
_logger = logger;
}
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (args.Parent is Series series && args.IsDirectory)
{
- var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
+ var namingOptions = _namingOptions;
var path = args.Path;
@@ -65,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var episodeInfo = resolver.Resolve(testPath, true);
- if (episodeInfo != null)
+ if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue)
{
- if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
- {
- _logger.LogDebug(
- "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
- path,
- episodeInfo.SeasonNumber.Value,
- episodeInfo.EpisodeNumber.Value);
-
- return null;
- }
+ _logger.LogDebug(
+ "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
+ path,
+ episodeInfo.SeasonNumber.Value,
+ episodeInfo.EpisodeNumber.Value);
+
+ return null;
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index a1562abd3..f5ac3c665 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -5,10 +5,11 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Emby.Naming.Common;
using Emby.Naming.TV;
+using Emby.Naming.Video;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -19,20 +20,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary>
/// Class SeriesResolver.
/// </summary>
- public class SeriesResolver : FolderResolver<Series>
+ public class SeriesResolver : GenericFolderResolver<Series>
{
private readonly ILogger<SeriesResolver> _logger;
- private readonly ILibraryManager _libraryManager;
+ private readonly NamingOptions _namingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
- /// <param name="libraryManager">The library manager.</param>
- public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
+ /// <param name="namingOptions">The naming options.</param>
+ public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
{
_logger = logger;
- _libraryManager = libraryManager;
+ _namingOptions = namingOptions;
}
/// <summary>
@@ -55,16 +56,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
+ var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
+
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
- var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
+ // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
+ var configuredContentType = args.GetConfiguredContentType();
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
return new Series
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = seriesInfo.Name
};
}
}
@@ -81,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return new Series
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = seriesInfo.Name
};
}
@@ -90,12 +94,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
- if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
+ if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
{
return new Series
{
Path = args.Path,
- Name = Path.GetFileName(args.Path)
+ Name = seriesInfo.Name
};
}
}
@@ -104,11 +108,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
- public static bool IsSeriesFolder(
+ private bool IsSeriesFolder(
string path,
IEnumerable<FileSystemMetadata> fileSystemChildren,
- ILogger<SeriesResolver> logger,
- ILibraryManager libraryManager,
bool isTvContentType)
{
foreach (var child in fileSystemChildren)
@@ -117,21 +119,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (IsSeasonFolder(child.FullName, isTvContentType))
{
- logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
+ _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
}
}
else
{
string fullName = child.FullName;
- if (libraryManager.IsVideoFile(fullName))
+ if (VideoResolver.IsVideoFile(path, _namingOptions))
{
if (isTvContentType)
{
return true;
}
- var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
+ var namingOptions = _namingOptions;
var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions);
@@ -144,7 +146,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
}
}
- logger.LogDebug("{Path} is not a series folder.", path);
+ _logger.LogDebug("{Path} is not a series folder.", path);
return false;
}
@@ -180,13 +182,42 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <param name="path">The path.</param>
private static void SetProviderIdFromPath(Series item, string path)
{
- var justName = Path.GetFileName(path);
+ var justName = Path.GetFileName(path.AsSpan());
+
+ var tvdbId = justName.GetAttributeValue("tvdbid");
+ if (!string.IsNullOrEmpty(tvdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
+ }
- var id = justName.GetAttributeValue("tvdbid");
+ var tvmazeId = justName.GetAttributeValue("tvmazeid");
+ if (!string.IsNullOrEmpty(tvmazeId))
+ {
+ item.SetProviderId(MetadataProvider.TvMaze, tvmazeId);
+ }
+
+ var tmdbId = justName.GetAttributeValue("tmdbid");
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
+ }
+
+ var anidbId = justName.GetAttributeValue("anidbid");
+ if (!string.IsNullOrEmpty(anidbId))
+ {
+ item.SetProviderId("AniDB", anidbId);
+ }
+
+ var aniListId = justName.GetAttributeValue("anilistid");
+ if (!string.IsNullOrEmpty(aniListId))
+ {
+ item.SetProviderId("AniList", aniListId);
+ }
- if (!string.IsNullOrEmpty(id))
+ var aniSearchId = justName.GetAttributeValue("anisearchid");
+ if (!string.IsNullOrEmpty(aniSearchId))
{
- item.SetProviderId(MetadataProvider.Tvdb, id);
+ item.SetProviderId("AniSearch", aniSearchId);
}
}
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 26e615fa0..4aacf7774 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -5,17 +5,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
-using Genre = MediaBrowser.Controller.Entities.Genre;
-using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
@@ -59,9 +56,9 @@ namespace Emby.Server.Implementations.Library
};
}
- private static void AddIfMissing(List<string> list, string value)
+ private static void AddIfMissing(List<BaseItemKind> list, BaseItemKind value)
{
- if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
+ if (!list.Contains(value))
{
list.Add(value);
}
@@ -73,7 +70,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="query">The query.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable{SearchHintResult}.</returns>
- /// <exception cref="ArgumentNullException">searchTerm</exception>
+ /// <exception cref="ArgumentException"><c>query.SearchTerm</c> is <c>null</c> or empty.</exception>
private List<SearchHintInfo> GetSearchHints(SearchQuery query, User user)
{
var searchTerm = query.SearchTerm;
@@ -86,63 +83,63 @@ namespace Emby.Server.Implementations.Library
searchTerm = searchTerm.Trim().RemoveDiacritics();
var excludeItemTypes = query.ExcludeItemTypes.ToList();
- var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
+ var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<BaseItemKind>()).ToList();
- excludeItemTypes.Add(nameof(Year));
- excludeItemTypes.Add(nameof(Folder));
+ excludeItemTypes.Add(BaseItemKind.Year);
+ excludeItemTypes.Add(BaseItemKind.Folder);
- if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Genre)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, nameof(Genre));
- AddIfMissing(includeItemTypes, nameof(MusicGenre));
+ AddIfMissing(includeItemTypes, BaseItemKind.Genre);
+ AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre);
}
}
else
{
- AddIfMissing(excludeItemTypes, nameof(Genre));
- AddIfMissing(excludeItemTypes, nameof(MusicGenre));
+ AddIfMissing(excludeItemTypes, BaseItemKind.Genre);
+ AddIfMissing(excludeItemTypes, BaseItemKind.MusicGenre);
}
- if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Person)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, nameof(Person));
+ AddIfMissing(includeItemTypes, BaseItemKind.Person);
}
}
else
{
- AddIfMissing(excludeItemTypes, nameof(Person));
+ AddIfMissing(excludeItemTypes, BaseItemKind.Person);
}
- if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Studio)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, nameof(Studio));
+ AddIfMissing(includeItemTypes, BaseItemKind.Studio);
}
}
else
{
- AddIfMissing(excludeItemTypes, nameof(Studio));
+ AddIfMissing(excludeItemTypes, BaseItemKind.Studio);
}
- if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
+ if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.MusicArtist)))
{
if (!query.IncludeMedia)
{
- AddIfMissing(includeItemTypes, nameof(MusicArtist));
+ AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist);
}
}
else
{
- AddIfMissing(excludeItemTypes, nameof(MusicArtist));
+ AddIfMissing(excludeItemTypes, BaseItemKind.MusicArtist);
}
- AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
- AddIfMissing(excludeItemTypes, nameof(Folder));
+ AddIfMissing(excludeItemTypes, BaseItemKind.CollectionFolder);
+ AddIfMissing(excludeItemTypes, BaseItemKind.Folder);
var mediaTypes = query.MediaTypes.ToList();
if (includeItemTypes.Count > 0)
@@ -183,7 +180,7 @@ namespace Emby.Server.Implementations.Library
List<BaseItem> mediaItems;
- if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase))
+ if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{
if (!searchQuery.ParentId.Equals(Guid.Empty))
{
@@ -192,7 +189,7 @@ namespace Emby.Server.Implementations.Library
searchQuery.ParentId = Guid.Empty;
searchQuery.IncludeItemsByName = true;
- searchQuery.IncludeItemTypes = Array.Empty<string>();
+ searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>();
mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList();
}
else
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 8aa605a90..bb3034142 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -25,8 +25,6 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
- public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
-
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
@@ -44,6 +42,8 @@ namespace Emby.Server.Implementations.Library
_repository = repository;
}
+ public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
+
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -90,10 +90,9 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary>
- /// <param name="userId"></param>
- /// <param name="userData"></param>
- /// <param name="cancellationToken"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userData">The user item data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
@@ -104,8 +103,8 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Retrieve all user data for the given user.
/// </summary>
- /// <param name="userId"></param>
- /// <returns></returns>
+ /// <param name="userId">The user id.</param>
+ /// <returns>A <see cref="List{UserItemData}"/> containing all of the user's item data.</returns>
public List<UserItemData> GetAllUserData(Guid userId)
{
var user = _userManager.GetUserById(userId);
@@ -177,6 +176,7 @@ namespace Emby.Server.Implementations.Library
return dto;
}
+ /// <inheritdoc />
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="data">The data.</param>
/// <returns>DtoUserItemData.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
private UserItemDataDto GetUserItemDataDto(UserItemData data)
{
if (data == null)
@@ -212,6 +212,7 @@ namespace Emby.Server.Implementations.Library
};
}
+ /// <inheritdoc />
public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
{
var playedToCompletion = false;
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e2da672a3..ab8bc6328 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -8,11 +8,11 @@ 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;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Channels;
@@ -20,8 +20,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
-using Genre = MediaBrowser.Controller.Entities.Genre;
-using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
@@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Library
continue;
}
- if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
list.Add(GetUserView(folder, folderViewType, string.Empty));
}
@@ -180,7 +178,7 @@ namespace Emby.Server.Implementations.Library
{
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
{
- if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase))
+ if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
{
return (Folder)parents[0];
}
@@ -300,11 +298,11 @@ namespace Emby.Server.Implementations.Library
{
if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
{
- includeItemTypes = new string[] { "Movie" };
+ includeItemTypes = new[] { BaseItemKind.Movie };
}
else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
{
- includeItemTypes = new string[] { "Episode" };
+ includeItemTypes = new[] { BaseItemKind.Episode };
}
}
}
@@ -341,19 +339,26 @@ namespace Emby.Server.Implementations.Library
mediaTypes = mediaTypes.Distinct().ToList();
}
- var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
- {
- nameof(Person),
- nameof(Studio),
- nameof(Year),
- nameof(MusicGenre),
- nameof(Genre)
- } : Array.Empty<string>();
+ var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
+ ? new[]
+ {
+ BaseItemKind.Person,
+ BaseItemKind.Studio,
+ BaseItemKind.Year,
+ BaseItemKind.MusicGenre,
+ BaseItemKind.Genre
+ }
+ : Array.Empty<BaseItemKind>();
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
- OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
+ OrderBy = new[]
+ {
+ (ItemSortBy.DateCreated, SortOrder.Descending),
+ (ItemSortBy.SortName, SortOrder.Descending),
+ (ItemSortBy.ProductionYear, SortOrder.Descending)
+ },
IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null,
ExcludeItemTypes = excludeItemTypes,
IsVirtualItem = false,
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index f9a3e2c64..7591e8391 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -3,6 +3,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(MusicArtist) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
@@ -95,10 +96,13 @@ namespace Emby.Server.Implementations.Library.Validators
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- }, false);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
progress.Report(100);
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
new file mode 100644
index 000000000..73e58d16c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Querying;
+using Jellyfin.Data.Enums;
+using Microsoft.Extensions.Logging;
+using MediaBrowser.Model.Entities;
+
+namespace Emby.Server.Implementations.Library.Validators
+{
+ /// <summary>
+ /// Class CollectionPostScanTask.
+ /// </summary>
+ public class CollectionPostScanTask : ILibraryPostScanTask
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ICollectionManager _collectionManager;
+ private readonly ILogger<CollectionPostScanTask> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="collectionManager">The collection manager.</param>
+ /// <param name="logger">The logger.</param>
+ public CollectionPostScanTask(
+ ILibraryManager libraryManager,
+ ICollectionManager collectionManager,
+ ILogger<CollectionPostScanTask> logger)
+ {
+ _libraryManager = libraryManager;
+ _collectionManager = collectionManager;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
+
+ foreach (var library in _libraryManager.RootFolder.Children)
+ {
+ if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
+ {
+ continue;
+ }
+
+ var startIndex = 0;
+ var pagesize = 1000;
+
+ while (true)
+ {
+ var movies = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ MediaTypes = new string[] { MediaType.Video },
+ IncludeItemTypes = new[] { BaseItemKind.Movie },
+ IsVirtualItem = false,
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
+ Parent = library,
+ StartIndex = startIndex,
+ Limit = pagesize,
+ Recursive = true
+ });
+
+ foreach (var m in movies)
+ {
+ if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
+ {
+ if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
+ {
+ movieList.Add(movie.Id);
+ }
+ else
+ {
+ collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
+ }
+ }
+ }
+
+ if (movies.Count < pagesize)
+ {
+ break;
+ }
+
+ startIndex += pagesize;
+ }
+ }
+
+ var numComplete = 0;
+ var count = collectionNameMoviesMap.Count;
+
+ if (count == 0)
+ {
+ progress.Report(100);
+ return;
+ }
+
+ var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { BaseItemKind.BoxSet },
+ CollapseBoxSetItems = false,
+ Recursive = true
+ });
+
+ foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+ {
+ try
+ {
+ var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
+ if (boxSet == null)
+ {
+ // won't automatically create collection if only one movie in it
+ if (movieIds.Count >= 2)
+ {
+ boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
+ {
+ Name = collectionName,
+ IsLocked = true
+ });
+
+ await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
+ }
+ }
+ else
+ {
+ await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+
+ progress.Report(percent);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+ }
+ }
+
+ progress.Report(100);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
index 8739a9e1b..601aab5b9 100644
--- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
@@ -2,6 +2,7 @@ using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -78,7 +79,7 @@ namespace Emby.Server.Implementations.Library.Validators
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error validating IBN entry {person}", person);
+ _logger.LogError(ex, "Error validating IBN entry {Person}", person);
}
// Update progress
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Person) },
+ IncludeItemTypes = new[] { BaseItemKind.Person },
IsDeadPerson = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index 9a8c5f39d..26bc49c1f 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -2,6 +2,7 @@ using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
@@ -80,19 +81,22 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Studio) },
+ IncludeItemTypes = new[] { BaseItemKind.Studio },
IsDeadStudio = true,
IsLocked = false
});
foreach (var item in deadEntities)
{
- _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
+ _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- }, false);
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false
+ },
+ false);
}
progress.Report(100);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 3fcadf5b1..6937cc097 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -7,6 +5,7 @@ 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.Model.Dto;
@@ -33,7 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return targetFile;
}
- public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
+ public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
if (directStreamProvider != null)
{
@@ -45,23 +44,29 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
- Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+ Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
onStarted();
- _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+ _logger.LogInformation("Copying recording to file {FilePath}", targetFile);
// The media source is infinite so we need to handle stopping ourselves
using var durationToken = new CancellationTokenSource(duration);
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
-
- await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
+ var linkedCancellationToken = cancellationTokenSource.Token;
+
+ await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream());
+ await _streamHelper.CopyToAsync(
+ fileStream,
+ output,
+ IODefaults.CopyToBufferSize,
+ 1000,
+ linkedCancellationToken).ConfigureAwait(false);
}
- _logger.LogInformation("Recording completed to file {0}", targetFile);
+ _logger.LogInformation("Recording completed: {FilePath}", targetFile);
}
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
@@ -71,10 +76,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Opened recording stream from tuner provider");
- Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+ Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile)));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
+ await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous);
onStarted();
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 797063120..7ef93d166 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -17,6 +17,7 @@ using System.Xml;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
@@ -159,8 +160,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
var recordingFolders = GetRecordingFolders().ToArray();
- var virtualFolders = _libraryManager.GetVirtualFolders()
- .ToList();
+ var virtualFolders = _libraryManager.GetVirtualFolders();
var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
continue;
}
- var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
+ var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
var libraryOptions = new LibraryOptions
{
@@ -210,7 +210,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
foreach (var path in pathsToRemove)
{
- await RemovePathFromLibrary(path).ConfigureAwait(false);
+ await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -219,17 +219,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private async Task RemovePathFromLibrary(string path)
+ private async Task RemovePathFromLibraryAsync(string path)
{
_logger.LogDebug("Removing path from library: {0}", path);
var requiresRefresh = false;
- var virtualFolders = _libraryManager.GetVirtualFolders()
- .ToList();
+ var virtualFolders = _libraryManager.GetVirtualFolders();
foreach (var virtualFolder in virtualFolders)
{
- if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
+ if (!virtualFolder.Locations.Contains(path, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -460,7 +459,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
{
var tunerChannelId = tunerChannel.TunerChannelId;
- if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
+ if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
{
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
}
@@ -612,16 +611,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
- public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
+ public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
{
- var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
+ var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
null :
- _timerProvider.GetTimerByProgramId(timer.ProgramId);
+ _timerProvider.GetTimerByProgramId(info.ProgramId);
if (existingTimer != null)
{
- if (existingTimer.Status == RecordingStatus.Cancelled ||
- existingTimer.Status == RecordingStatus.Completed)
+ if (existingTimer.Status == RecordingStatus.Cancelled
+ || existingTimer.Status == RecordingStatus.Completed)
{
existingTimer.Status = RecordingStatus.New;
existingTimer.IsManual = true;
@@ -634,32 +633,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
LiveTvProgram programInfo = null;
- if (!string.IsNullOrWhiteSpace(timer.ProgramId))
+ if (!string.IsNullOrWhiteSpace(info.ProgramId))
{
- programInfo = GetProgramInfoFromCache(timer);
+ programInfo = GetProgramInfoFromCache(info);
}
if (programInfo == null)
{
- _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
- programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
+ _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
+ programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
}
if (programInfo != null)
{
- CopyProgramInfoToTimerInfo(programInfo, timer);
+ CopyProgramInfoToTimerInfo(programInfo, info);
}
- timer.IsManual = true;
- _timerProvider.Add(timer);
+ info.IsManual = true;
+ _timerProvider.Add(info);
- TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
- return Task.FromResult(timer.Id);
+ return Task.FromResult(info.Id);
}
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
@@ -893,7 +892,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new ArgumentNullException(nameof(tunerHostId));
}
- return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
+ return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase);
}
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
@@ -913,18 +912,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
- List<ProgramInfo> programs;
-
if (epgChannel == null)
{
_logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
- programs = new List<ProgramInfo>();
+ continue;
}
- else
- {
- programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
+
+ List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
.ConfigureAwait(false)).ToList();
- }
// Replace the value that came from the provider with a normalized value
foreach (var program in programs)
@@ -940,7 +935,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- return new List<ProgramInfo>();
+ return Enumerable.Empty<ProgramInfo>();
}
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
@@ -963,7 +958,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- _logger.LogInformation("Streaming Channel " + channelId);
+ _logger.LogInformation("Streaming Channel {Id}", channelId);
var result = string.IsNullOrEmpty(streamId) ?
null :
@@ -1033,7 +1028,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var stream = new MediaSourceInfo
{
- EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+ EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
EncoderProtocol = MediaProtocol.Http,
Path = info.Path,
Protocol = MediaProtocol.File,
@@ -1292,7 +1287,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
- _logger.LogInformation("Writing file to path: " + recordPath);
+ _logger.LogInformation("Writing file to: {Path}", recordPath);
Action onStarted = async () =>
{
@@ -1314,16 +1309,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
recordingStatus = RecordingStatus.Completed;
- _logger.LogInformation("Recording completed: {recordPath}", recordPath);
+ _logger.LogInformation("Recording completed: {RecordPath}", recordPath);
}
catch (OperationCanceledException)
{
- _logger.LogInformation("Recording stopped: {recordPath}", recordPath);
+ _logger.LogInformation("Recording stopped: {RecordPath}", recordPath);
recordingStatus = RecordingStatus.Completed;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error recording to {recordPath}", recordPath);
+ _logger.LogError(ex, "Error recording to {RecordPath}", recordPath);
recordingStatus = RecordingStatus.Error;
}
@@ -1410,20 +1405,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error deleting 0-byte failed recording file {path}", path);
+ _logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path);
}
}
}
private void TriggerRefresh(string path)
{
- _logger.LogInformation("Triggering refresh on {path}", path);
+ _logger.LogInformation("Triggering refresh on {Path}", path);
var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
if (item != null)
{
- _logger.LogInformation("Refreshing recording parent {path}", item.Path);
+ _logger.LogInformation("Refreshing recording parent {Path}", item.Path);
_providerManager.QueueRefresh(
item.Id,
@@ -1458,7 +1453,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase))
{
var parentItem = item.GetParent();
- if (parentItem != null && !(parentItem is AggregateFolder))
+ if (parentItem != null && parentItem is not AggregateFolder)
{
item = parentItem;
}
@@ -1512,8 +1507,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
DeleteLibraryItemsForTimers(timersToDelete);
- var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
- if (librarySeries == null)
+ if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
{
return;
}
@@ -1667,7 +1661,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
- process.Exited += Process_Exited;
+ process.Exited += OnProcessExited;
process.Start();
}
catch (Exception ex)
@@ -1681,7 +1675,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
}
- private void Process_Exited(object sender, EventArgs e)
+ private void OnProcessExited(object sender, EventArgs e)
{
using (var process = (Process)sender)
{
@@ -1785,7 +1779,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
Limit = 1,
ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true)
@@ -1855,14 +1849,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
using (var writer = XmlWriter.Create(stream, settings))
@@ -1920,14 +1912,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
Indent = true,
- Encoding = Encoding.UTF8,
- CloseOutput = false
+ Encoding = Encoding.UTF8
};
var options = _config.GetNfoConfiguration();
@@ -1997,7 +1987,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString(
"dateadded",
- DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
+ DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
if (item.ProductionYear.HasValue)
{
@@ -2148,7 +2138,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
Limit = 1,
DtoOptions = new DtoOptions(true)
{
@@ -2239,7 +2229,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
- var existingTimer = _timerProvider.GetTimer(timer.Id)
+ var existingTimer = _timerProvider.GetTimer(timer.Id)
?? (string.IsNullOrWhiteSpace(timer.ProgramId)
? null
: _timerProvider.GetTimerByProgramId(timer.ProgramId));
@@ -2343,7 +2333,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var deletes = _timerProvider.GetAll()
.Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
- .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
+ .Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
.Where(i => deleteStatuses.Contains(i.Status))
.ToList();
@@ -2363,7 +2353,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true)
{
@@ -2398,7 +2388,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@@ -2457,7 +2447,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList(
new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel;
@@ -2522,7 +2512,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Series) },
+ IncludeItemTypes = new[] { BaseItemKind.Series },
Name = program.Name
}).ToArray();
@@ -2535,7 +2525,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var result = _libraryManager.GetItemIds(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds,
@@ -2632,7 +2622,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (newDevicesOnly)
{
- discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
+ discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase))
.ToList();
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 26e4ef1ed..a88a1fe84 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -11,9 +11,9 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
@@ -87,17 +87,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ErrorDialog = false
};
- var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments;
- _logger.LogInformation(commandLineLogMessage);
+ _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments);
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
- await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
+ await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
_process = new Process
{
@@ -188,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile,
- targetFile,
+ targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
@@ -205,9 +204,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// var audioChannels = 2;
// var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
// if (audioStream != null)
- //{
+ // {
// audioChannels = audioStream.Channels ?? audioChannels;
- //}
+ // }
// return "-codec:a:0 aac -strict experimental -ab 320000";
}
@@ -225,13 +224,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
try
{
- _logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath);
+ _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath);
_process.StandardInput.WriteLine("q");
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error stopping recording transcoding job for {path}", _targetPath);
+ _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath);
}
if (_hasExited)
@@ -241,7 +240,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- _logger.LogInformation("Calling recording process.WaitForExit for {path}", _targetPath);
+ _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath);
if (_process.WaitForExit(10000))
{
@@ -250,7 +249,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error waiting for recording process to exit for {path}", _targetPath);
+ _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath);
}
if (_hasExited)
@@ -260,13 +259,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
- _logger.LogInformation("Killing ffmpeg recording process for {path}", _targetPath);
+ _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath);
_process.Kill();
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error killing recording transcoding job for {path}", _targetPath);
+ _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath);
}
}
}
@@ -319,11 +318,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
}
- catch (ObjectDisposedException)
- {
- // TODO Investigate and properly fix.
- // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
- }
catch (Exception ex)
{
_logger.LogError(ex, "Error reading ffmpeg recording log");
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
index 0ec52a959..20a8213a7 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs
@@ -8,7 +8,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
internal class EpgChannelData
{
-
private readonly Dictionary<string, ChannelInfo> _channelsById;
private readonly Dictionary<string, ChannelInfo> _channelsByNumber;
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
index 4712724d6..7705132da 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs
@@ -13,7 +13,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Records the specified media source.
/// </summary>
- Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
+ /// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param>
+ /// <param name="mediaSource">The media source.</param>
+ /// <param name="targetFile">The target file.</param>
+ /// <param name="duration">The duration to record.</param>
+ /// <param name="onStarted">An action to perform when recording starts.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A <see cref="Task"/> that represents the recording operation.</returns>
+ Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
index bdab8c3e4..46979bfc5 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
@@ -1,13 +1,12 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -18,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private T[] _items;
+ private T[]? _items;
public ItemDataProvider(
ILogger logger,
@@ -34,6 +33,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
protected Func<T, T, bool> EqualityComparer { get; }
+ [MemberNotNull(nameof(_items))]
private void EnsureLoaded()
{
if (_items != null)
@@ -49,6 +49,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var bytes = File.ReadAllBytes(_dataPath);
_items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
+ if (_items == null)
+ {
+ Logger.LogError("Error deserializing {Path}, data was null", _dataPath);
+ _items = Array.Empty<T>();
+ }
+
return;
}
catch (JsonException ex)
@@ -62,7 +68,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
- Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath)));
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
File.WriteAllText(_dataPath, jsonString);
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 6c52a9a73..a861e6ae4 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -23,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
}
- public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
+ public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired;
public void RestartTimers()
{
@@ -145,9 +143,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private void TimerCallback(object state)
+ private void TimerCallback(object? state)
{
- var timerId = (string)state;
+ var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state));
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (timer != null)
@@ -156,12 +154,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- public TimerInfo GetTimer(string id)
+ public TimerInfo? GetTimer(string id)
{
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
}
- public TimerInfo GetTimerByProgramId(string programId)
+ public TimerInfo? GetTimerByProgramId(string programId)
{
return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 00d02873c..dd0cb6c5d 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -9,16 +9,18 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Json;
+using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -33,8 +35,6 @@ 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 IApplicationHost _appHost;
- private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -42,14 +42,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
- IHttpClientFactory httpClientFactory,
- IApplicationHost appHost,
- ICryptoProvider cryptoProvider)
+ IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
- _cryptoProvider = cryptoProvider;
}
/// <inheritdoc />
@@ -96,78 +92,92 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
- var requestList = new List<ScheduleDirect.RequestScheduleForChannel>()
+ var requestList = new List<RequestScheduleForChannelDto>()
{
- new ScheduleDirect.RequestScheduleForChannel()
+ new RequestScheduleForChannelDto()
{
- stationID = channelId,
- date = dates
+ StationId = channelId,
+ Date = dates
}
};
- var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
- _logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
+ _logger.LogDebug("Request string for schedules is: {@RequestString}", requestList);
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
- options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
+ options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (dailySchedules == null)
+ {
+ return Array.Empty<ProgramInfo>();
+ }
+
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
- var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
- programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
+ 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);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- var programDict = programDetails.ToDictionary(p => p.programID, y => y);
+ var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (programDetails == null)
+ {
+ return Array.Empty<ProgramInfo>();
+ }
+
+ var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails
- .Where(p => p.hasImageArtwork).Select(p => p.programID)
+ .Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
.ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
var programsInfo = new List<ProgramInfo>();
- foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+ foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
{
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
// " which corresponds to channel " + channelNumber + " and program id " +
- // schedule.programID + " which says it has images? " +
- // programDict[schedule.programID].hasImageArtwork);
+ // schedule.ProgramId + " which says it has images? " +
+ // programDict[schedule.ProgramId].hasImageArtwork);
+
+ if (string.IsNullOrEmpty(schedule.ProgramId))
+ {
+ continue;
+ }
if (images != null)
{
- var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
+ var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
if (imageIndex > -1)
{
- var programEntry = programDict[schedule.programID];
+ var programEntry = programDict[schedule.ProgramId];
- var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
- var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
- var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
+ var allImages = images[imageIndex].Data;
+ var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase));
+ var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase));
const double DesiredAspect = 2.0 / 3;
- programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
- GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
+ programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
+ GetProgramImage(ApiUrl, allImages, DesiredAspect);
const double WideAspect = 16.0 / 9;
- programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
+ programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
// Don't supply the same image twice
- if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
+ if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
{
- programEntry.thumbImage = null;
+ programEntry.ThumbImage = null;
}
- programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
+ programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@@ -176,15 +186,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
+ programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.ProgramId]));
}
return programsInfo;
}
- private static int GetSizeOrder(ScheduleDirect.ImageData image)
+ private static int GetSizeOrder(ImageDataDto image)
{
- if (int.TryParse(image.height, out int value))
+ if (int.TryParse(image.Height, out int value))
{
return value;
}
@@ -192,53 +202,58 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return 0;
}
- private static string GetChannelNumber(ScheduleDirect.Map map)
+ private static string GetChannelNumber(MapDto map)
{
- var channelNumber = map.logicalChannelNumber;
+ var channelNumber = map.LogicalChannelNumber;
if (string.IsNullOrWhiteSpace(channelNumber))
{
- channelNumber = map.channel;
+ channelNumber = map.Channel;
}
if (string.IsNullOrWhiteSpace(channelNumber))
{
- channelNumber = map.atscMajor + "." + map.atscMinor;
+ channelNumber = map.AtscMajor + "." + map.AtscMinor;
}
return channelNumber.TrimStart('0');
}
- private static bool IsMovie(ScheduleDirect.ProgramDetails programInfo)
+ private static bool IsMovie(ProgramDetailsDto programInfo)
{
- return string.Equals(programInfo.entityType, "movie", StringComparison.OrdinalIgnoreCase);
+ return string.Equals(programInfo.EntityType, "movie", StringComparison.OrdinalIgnoreCase);
}
- private ProgramInfo GetProgram(string channelId, ScheduleDirect.Program programInfo, ScheduleDirect.ProgramDetails details)
+ private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
{
- var startAt = GetDate(programInfo.airDateTime);
- var endAt = startAt.AddSeconds(programInfo.duration);
+ if (programInfo.AirDateTime == null)
+ {
+ return null;
+ }
+
+ var startAt = programInfo.AirDateTime.Value;
+ var endAt = startAt.AddSeconds(programInfo.Duration);
var audioType = ProgramAudio.Stereo;
- var programId = programInfo.programID ?? string.Empty;
+ var programId = programInfo.ProgramId ?? string.Empty;
string newID = programId + "T" + startAt.Ticks + "C" + channelId;
- if (programInfo.audioProperties != null)
+ if (programInfo.AudioProperties.Count != 0)
{
- if (programInfo.audioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
+ if (programInfo.AudioProperties.Contains("atmos", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Atmos;
}
- else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("dd", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.audioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
+ else if (programInfo.AudioProperties.Contains("stereo", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Stereo;
}
@@ -249,9 +264,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
string episodeTitle = null;
- if (details.episodeTitle150 != null)
+ if (details.EpisodeTitle150 != null)
{
- episodeTitle = details.episodeTitle150;
+ episodeTitle = details.EpisodeTitle150;
}
var info = new ProgramInfo
@@ -260,22 +275,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Id = newID,
StartDate = startAt,
EndDate = endAt,
- Name = details.titles[0].title120 ?? "Unknown",
+ Name = details.Titles[0].Title120 ?? "Unknown",
OfficialRating = null,
CommunityRating = null,
EpisodeTitle = episodeTitle,
Audio = audioType,
// IsNew = programInfo.@new ?? false,
- IsRepeat = programInfo.@new == null,
- IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase),
- ImageUrl = details.primaryImage,
- ThumbImageUrl = details.thumbImage,
- IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase),
- IsSports = string.Equals(details.entityType, "sports", StringComparison.OrdinalIgnoreCase),
+ IsRepeat = programInfo.New == null,
+ IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase),
+ ImageUrl = details.PrimaryImage,
+ ThumbImageUrl = details.ThumbImage,
+ IsKids = string.Equals(details.Audience, "children", StringComparison.OrdinalIgnoreCase),
+ IsSports = string.Equals(details.EntityType, "sports", StringComparison.OrdinalIgnoreCase),
IsMovie = IsMovie(details),
- Etag = programInfo.md5,
- IsLive = string.Equals(programInfo.liveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
- IsPremiere = programInfo.premiere || (programInfo.isPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
+ Etag = programInfo.Md5,
+ IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
+ IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
};
var showId = programId;
@@ -298,33 +313,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
info.ShowId = showId;
- if (programInfo.videoProperties != null)
+ if (programInfo.VideoProperties != null)
{
- info.IsHD = programInfo.videoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
- info.Is3D = programInfo.videoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
+ info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparison.OrdinalIgnoreCase);
+ info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparison.OrdinalIgnoreCase);
}
- if (details.contentRating != null && details.contentRating.Count > 0)
+ if (details.ContentRating != null && details.ContentRating.Count > 0)
{
- info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-", StringComparison.Ordinal)
+ info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal)
.Replace("--", "-", StringComparison.Ordinal);
var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
- if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase))
+ if (invalid.Contains(info.OfficialRating, StringComparison.OrdinalIgnoreCase))
{
info.OfficialRating = null;
}
}
- if (details.descriptions != null)
+ if (details.Descriptions != null)
{
- if (details.descriptions.description1000 != null && details.descriptions.description1000.Count > 0)
+ if (details.Descriptions.Description1000 != null && details.Descriptions.Description1000.Count > 0)
{
- info.Overview = details.descriptions.description1000[0].description;
+ info.Overview = details.Descriptions.Description1000[0].Description;
}
- else if (details.descriptions.description100 != null && details.descriptions.description100.Count > 0)
+ else if (details.Descriptions.Description100 != null && details.Descriptions.Description100.Count > 0)
{
- info.Overview = details.descriptions.description100[0].description;
+ info.Overview = details.Descriptions.Description100[0].Description;
}
}
@@ -334,18 +349,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings
info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId;
- if (details.metadata != null)
+ if (details.Metadata != null)
{
- foreach (var metadataProgram in details.metadata)
+ foreach (var metadataProgram in details.Metadata)
{
var gracenote = metadataProgram.Gracenote;
if (gracenote != null)
{
- info.SeasonNumber = gracenote.season;
+ info.SeasonNumber = gracenote.Season;
- if (gracenote.episode > 0)
+ if (gracenote.Episode > 0)
{
- info.EpisodeNumber = gracenote.episode;
+ info.EpisodeNumber = gracenote.Episode;
}
break;
@@ -354,27 +369,27 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- if (!string.IsNullOrWhiteSpace(details.originalAirDate))
+ if (details.OriginalAirDate != null)
{
- info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
+ info.OriginalAirDate = details.OriginalAirDate;
info.ProductionYear = info.OriginalAirDate.Value.Year;
}
- if (details.movie != null)
+ if (details.Movie != null)
{
- if (!string.IsNullOrEmpty(details.movie.year)
- && int.TryParse(details.movie.year, out int year))
+ if (!string.IsNullOrEmpty(details.Movie.Year)
+ && int.TryParse(details.Movie.Year, out int year))
{
info.ProductionYear = year;
}
}
- if (details.genres != null)
+ if (details.Genres != null)
{
- info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
- info.IsNews = details.genres.Contains("news", StringComparer.OrdinalIgnoreCase);
+ info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
+ info.IsNews = details.Genres.Contains("news", StringComparison.OrdinalIgnoreCase);
- if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase))
+ if (info.Genres.Contains("children", StringComparison.OrdinalIgnoreCase))
{
info.IsKids = true;
}
@@ -383,23 +398,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info;
}
- private static DateTime GetDate(string value)
- {
- var date = DateTime.ParseExact(value, "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'", CultureInfo.InvariantCulture);
-
- if (date.Kind != DateTimeKind.Utc)
- {
- date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
- }
-
- return date;
- }
-
- private string GetProgramImage(string apiUrl, IEnumerable<ScheduleDirect.ImageData> images, bool returnDefaultImage, double desiredAspect)
+ private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
{
var match = images
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
- .ThenByDescending(GetSizeOrder)
+ .ThenByDescending(i => GetSizeOrder(i))
.FirstOrDefault();
if (match == null)
@@ -407,7 +410,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null;
}
- var uri = match.uri;
+ var uri = match.Uri;
if (string.IsNullOrWhiteSpace(uri))
{
@@ -423,19 +426,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- private static double GetAspectRatio(ScheduleDirect.ImageData i)
+ private static double GetAspectRatio(ImageDataDto i)
{
int width = 0;
int height = 0;
- if (!string.IsNullOrWhiteSpace(i.width))
+ if (!string.IsNullOrWhiteSpace(i.Width))
{
- int.TryParse(i.width, out width);
+ _ = int.TryParse(i.Width, out width);
}
- if (!string.IsNullOrWhiteSpace(i.height))
+ if (!string.IsNullOrWhiteSpace(i.Height))
{
- int.TryParse(i.height, out height);
+ _ = int.TryParse(i.Height, out height);
}
if (height == 0 || width == 0)
@@ -448,14 +451,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return result;
}
- private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
+ private async Task<IReadOnlyList<ShowImagesDto>> GetImageForPrograms(
ListingsProviderInfo info,
IReadOnlyList<string> programIds,
CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
- return new List<ScheduleDirect.ShowImages>();
+ return Array.Empty<ShowImagesDto>();
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -479,13 +482,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ShowImages>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting image info from schedules direct");
- return new List<ScheduleDirect.ShowImages>();
+ return Array.Empty<ShowImagesDto>();
}
}
@@ -508,18 +511,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Headends>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root != null)
{
- foreach (ScheduleDirect.Headends headend in root)
+ foreach (HeadendsDto headend in root)
{
- foreach (ScheduleDirect.Lineup lineup in headend.lineups)
+ foreach (LineupDto lineup in headend.Lineups)
{
lineups.Add(new NameIdPair
{
- Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
- Id = lineup.uri.Substring(18)
+ Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
+ Id = lineup.Uri?[18..]
});
}
}
@@ -640,7 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken)
{
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
- var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
+ var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password));
// TODO: remove ToLower when Convert.ToHexString supports lowercase
// Schedules Direct requires the hex to be lowercase
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
@@ -649,14 +652,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Token>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(root.message, "OK", StringComparison.Ordinal))
+ var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
- _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
- return root.token;
+ _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
+ return root.Token;
}
- throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
+ throw new Exception("Could not authenticate with Schedules Direct Error: " + root.Message);
}
private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -705,14 +708,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
- var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
+ return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
{
// SchedulesDirect returns 400 if no lineups are configured.
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
+ if (ex.StatusCode is HttpStatusCode.BadRequest)
{
return false;
}
@@ -777,35 +780,39 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
+ var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return new List<ChannelInfo>();
+ }
+
+ _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count);
_logger.LogInformation("Mapping Stations to Channel");
- var allStations = root.stations ?? new List<ScheduleDirect.Station>();
+ var allStations = root.Stations;
- var map = root.map;
+ var map = root.Map;
var list = new List<ChannelInfo>(map.Count);
foreach (var channel in map)
{
var channelNumber = GetChannelNumber(channel);
- var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
- ?? new ScheduleDirect.Station
- {
- stationID = channel.stationID
- };
+ var stationIndex = allStations.FindIndex(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase));
+ var station = stationIndex == -1
+ ? new StationDto { StationId = channel.StationId }
+ : allStations[stationIndex];
var channelInfo = new ChannelInfo
{
- Id = station.stationID,
- CallSign = station.callsign,
+ Id = station.StationId,
+ CallSign = station.Callsign,
Number = channelNumber,
- Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
+ Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name
};
- if (station.logo != null)
+ if (station.Logo != null)
{
- channelInfo.ImageUrl = station.logo.URL;
+ channelInfo.ImageUrl = station.Logo.Url;
}
list.Add(channelInfo);
@@ -818,402 +825,5 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
}
-
- public class ScheduleDirect
- {
- public class Token
- {
- public int code { get; set; }
-
- public string message { get; set; }
-
- public string serverID { get; set; }
-
- public string token { get; set; }
- }
-
- public class Lineup
- {
- public string lineup { get; set; }
-
- public string name { get; set; }
-
- public string transport { get; set; }
-
- public string location { get; set; }
-
- public string uri { get; set; }
- }
-
- public class Lineups
- {
- public int code { get; set; }
-
- public string serverID { get; set; }
-
- public string datetime { get; set; }
-
- public List<Lineup> lineups { get; set; }
- }
-
- public class Headends
- {
- public string headend { get; set; }
-
- public string transport { get; set; }
-
- public string location { get; set; }
-
- public List<Lineup> lineups { get; set; }
- }
-
- public class Map
- {
- public string stationID { get; set; }
-
- public string channel { get; set; }
-
- public string logicalChannelNumber { get; set; }
-
- public int uhfVhf { get; set; }
-
- public int atscMajor { get; set; }
-
- public int atscMinor { get; set; }
- }
-
- public class Broadcaster
- {
- public string city { get; set; }
-
- public string state { get; set; }
-
- public string postalcode { get; set; }
-
- public string country { get; set; }
- }
-
- public class Logo
- {
- public string URL { get; set; }
-
- public int height { get; set; }
-
- public int width { get; set; }
-
- public string md5 { get; set; }
- }
-
- public class Station
- {
- public string stationID { get; set; }
-
- public string name { get; set; }
-
- public string callsign { get; set; }
-
- public List<string> broadcastLanguage { get; set; }
-
- public List<string> descriptionLanguage { get; set; }
-
- public Broadcaster broadcaster { get; set; }
-
- public string affiliate { get; set; }
-
- public Logo logo { get; set; }
-
- public bool? isCommercialFree { get; set; }
- }
-
- public class Metadata
- {
- public string lineup { get; set; }
-
- public string modified { get; set; }
-
- public string transport { get; set; }
- }
-
- public class Channel
- {
- public List<Map> map { get; set; }
-
- public List<Station> stations { get; set; }
-
- public Metadata metadata { get; set; }
- }
-
- public class RequestScheduleForChannel
- {
- public string stationID { get; set; }
-
- public List<string> date { get; set; }
- }
-
- public class Rating
- {
- public string body { get; set; }
-
- public string code { get; set; }
- }
-
- public class Multipart
- {
- public int partNumber { get; set; }
-
- public int totalParts { get; set; }
- }
-
- public class Program
- {
- public string programID { get; set; }
-
- public string airDateTime { get; set; }
-
- public int duration { get; set; }
-
- public string md5 { get; set; }
-
- public List<string> audioProperties { get; set; }
-
- public List<string> videoProperties { get; set; }
-
- public List<Rating> ratings { get; set; }
-
- public bool? @new { get; set; }
-
- public Multipart multipart { get; set; }
-
- public string liveTapeDelay { get; set; }
-
- public bool premiere { get; set; }
-
- public bool repeat { get; set; }
-
- public string isPremiereOrFinale { get; set; }
- }
-
- public class MetadataSchedule
- {
- public string modified { get; set; }
-
- public string md5 { get; set; }
-
- public string startDate { get; set; }
-
- public string endDate { get; set; }
-
- public int days { get; set; }
- }
-
- public class Day
- {
- public string stationID { get; set; }
-
- public List<Program> programs { get; set; }
-
- public MetadataSchedule metadata { get; set; }
-
- public Day()
- {
- programs = new List<Program>();
- }
- }
-
- public class Title
- {
- public string title120 { get; set; }
- }
-
- public class EventDetails
- {
- public string subType { get; set; }
- }
-
- public class Description100
- {
- public string descriptionLanguage { get; set; }
-
- public string description { get; set; }
- }
-
- public class Description1000
- {
- public string descriptionLanguage { get; set; }
-
- public string description { get; set; }
- }
-
- public class DescriptionsProgram
- {
- public List<Description100> description100 { get; set; }
-
- public List<Description1000> description1000 { get; set; }
- }
-
- public class Gracenote
- {
- public int season { get; set; }
-
- public int episode { get; set; }
- }
-
- public class MetadataPrograms
- {
- public Gracenote Gracenote { get; set; }
- }
-
- public class ContentRating
- {
- public string body { get; set; }
-
- public string code { get; set; }
- }
-
- public class Cast
- {
- public string billingOrder { get; set; }
-
- public string role { get; set; }
-
- public string nameId { get; set; }
-
- public string personId { get; set; }
-
- public string name { get; set; }
-
- public string characterName { get; set; }
- }
-
- public class Crew
- {
- public string billingOrder { get; set; }
-
- public string role { get; set; }
-
- public string nameId { get; set; }
-
- public string personId { get; set; }
-
- public string name { get; set; }
- }
-
- public class QualityRating
- {
- public string ratingsBody { get; set; }
-
- public string rating { get; set; }
-
- public string minRating { get; set; }
-
- public string maxRating { get; set; }
-
- public string increment { get; set; }
- }
-
- public class Movie
- {
- public string year { get; set; }
-
- public int duration { get; set; }
-
- public List<QualityRating> qualityRating { get; set; }
- }
-
- public class Recommendation
- {
- public string programID { get; set; }
-
- public string title120 { get; set; }
- }
-
- public class ProgramDetails
- {
- public string audience { get; set; }
-
- public string programID { get; set; }
-
- public List<Title> titles { get; set; }
-
- public EventDetails eventDetails { get; set; }
-
- public DescriptionsProgram descriptions { get; set; }
-
- public string originalAirDate { get; set; }
-
- public List<string> genres { get; set; }
-
- public string episodeTitle150 { get; set; }
-
- public List<MetadataPrograms> metadata { get; set; }
-
- public List<ContentRating> contentRating { get; set; }
-
- public List<Cast> cast { get; set; }
-
- public List<Crew> crew { get; set; }
-
- public string entityType { get; set; }
-
- public string showType { get; set; }
-
- public bool hasImageArtwork { get; set; }
-
- public string primaryImage { get; set; }
-
- public string thumbImage { get; set; }
-
- public string backdropImage { get; set; }
-
- public string bannerImage { get; set; }
-
- public string imageID { get; set; }
-
- public string md5 { get; set; }
-
- public List<string> contentAdvisory { get; set; }
-
- public Movie movie { get; set; }
-
- public List<Recommendation> recommendations { get; set; }
- }
-
- public class Caption
- {
- public string content { get; set; }
-
- public string lang { get; set; }
- }
-
- public class ImageData
- {
- public string width { get; set; }
-
- public string height { get; set; }
-
- public string uri { get; set; }
-
- public string size { get; set; }
-
- public string aspect { get; set; }
-
- public string category { get; set; }
-
- public string text { get; set; }
-
- public string primary { get; set; }
-
- public string tier { get; set; }
-
- public Caption caption { get; set; }
- }
-
- public class ShowImages
- {
- public string programID { get; set; }
-
- public List<ImageData> data { get; set; }
- }
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
new file mode 100644
index 000000000..95ac996e0
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Broadcaster dto.
+ /// </summary>
+ public class BroadcasterDto
+ {
+ /// <summary>
+ /// Gets or sets the city.
+ /// </summary>
+ [JsonPropertyName("city")]
+ public string? City { get; set; }
+
+ /// <summary>
+ /// Gets or sets the state.
+ /// </summary>
+ [JsonPropertyName("state")]
+ public string? State { get; set; }
+
+ /// <summary>
+ /// Gets or sets the postal code.
+ /// </summary>
+ [JsonPropertyName("postalCode")]
+ public string? Postalcode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country.
+ /// </summary>
+ [JsonPropertyName("country")]
+ public string? Country { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
new file mode 100644
index 000000000..f6251b9ad
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Caption dto.
+ /// </summary>
+ public class CaptionDto
+ {
+ /// <summary>
+ /// Gets or sets the content.
+ /// </summary>
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ /// <summary>
+ /// Gets or sets the lang.
+ /// </summary>
+ [JsonPropertyName("lang")]
+ public string? Lang { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
new file mode 100644
index 000000000..0b7a2c63a
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Cast dto.
+ /// </summary>
+ public class CastDto
+ {
+ /// <summary>
+ /// Gets or sets the billing order.
+ /// </summary>
+ [JsonPropertyName("billingOrder")]
+ public string? BillingOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ [JsonPropertyName("role")]
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name id.
+ /// </summary>
+ [JsonPropertyName("nameId")]
+ public string? NameId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the person id.
+ /// </summary>
+ [JsonPropertyName("personId")]
+ public string? PersonId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the character name.
+ /// </summary>
+ [JsonPropertyName("characterName")]
+ public string? CharacterName { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
new file mode 100644
index 000000000..87c327ed8
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Channel dto.
+ /// </summary>
+ public class ChannelDto
+ {
+ /// <summary>
+ /// Gets or sets the list of maps.
+ /// </summary>
+ [JsonPropertyName("map")]
+ public IReadOnlyList<MapDto> Map { get; set; } = Array.Empty<MapDto>();
+
+ /// <summary>
+ /// Gets or sets the list of stations.
+ /// </summary>
+ [JsonPropertyName("stations")]
+ public IReadOnlyList<StationDto> Stations { get; set; } = Array.Empty<StationDto>();
+
+ /// <summary>
+ /// Gets or sets the metadata.
+ /// </summary>
+ [JsonPropertyName("metadata")]
+ public MetadataDto? Metadata { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
new file mode 100644
index 000000000..c19cd2e48
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Content rating dto.
+ /// </summary>
+ public class ContentRatingDto
+ {
+ /// <summary>
+ /// Gets or sets the body.
+ /// </summary>
+ [JsonPropertyName("body")]
+ public string? Body { get; set; }
+
+ /// <summary>
+ /// Gets or sets the code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
new file mode 100644
index 000000000..f00c9accd
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Crew dto.
+ /// </summary>
+ public class CrewDto
+ {
+ /// <summary>
+ /// Gets or sets the billing order.
+ /// </summary>
+ [JsonPropertyName("billingOrder")]
+ public string? BillingOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ [JsonPropertyName("role")]
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name id.
+ /// </summary>
+ [JsonPropertyName("nameId")]
+ public string? NameId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the person id.
+ /// </summary>
+ [JsonPropertyName("personId")]
+ public string? PersonId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
new file mode 100644
index 000000000..1a371965c
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Day dto.
+ /// </summary>
+ public class DayDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of programs.
+ /// </summary>
+ [JsonPropertyName("programs")]
+ public IReadOnlyList<ProgramDto> Programs { get; set; } = Array.Empty<ProgramDto>();
+
+ /// <summary>
+ /// Gets or sets the metadata schedule.
+ /// </summary>
+ [JsonPropertyName("metadata")]
+ public MetadataScheduleDto? Metadata { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
new file mode 100644
index 000000000..ca6ae7fb1
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Description 1_000 dto.
+ /// </summary>
+ public class Description1000Dto
+ {
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public string? DescriptionLanguage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
new file mode 100644
index 000000000..1577219ed
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Description 100 dto.
+ /// </summary>
+ public class Description100Dto
+ {
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public string? DescriptionLanguage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
new file mode 100644
index 000000000..eaf4a340b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Descriptions program dto.
+ /// </summary>
+ public class DescriptionsProgramDto
+ {
+ /// <summary>
+ /// Gets or sets the list of description 100.
+ /// </summary>
+ [JsonPropertyName("description100")]
+ public IReadOnlyList<Description100Dto> Description100 { get; set; } = Array.Empty<Description100Dto>();
+
+ /// <summary>
+ /// Gets or sets the list of description1000.
+ /// </summary>
+ [JsonPropertyName("description1000")]
+ public IReadOnlyList<Description1000Dto> Description1000 { get; set; } = Array.Empty<Description1000Dto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
new file mode 100644
index 000000000..fbdfb1f71
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Event details dto.
+ /// </summary>
+ public class EventDetailsDto
+ {
+ /// <summary>
+ /// Gets or sets the sub type.
+ /// </summary>
+ [JsonPropertyName("subType")]
+ public string? SubType { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
new file mode 100644
index 000000000..6852d89d7
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Gracenote dto.
+ /// </summary>
+ public class GracenoteDto
+ {
+ /// <summary>
+ /// Gets or sets the season.
+ /// </summary>
+ [JsonPropertyName("season")]
+ public int Season { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode.
+ /// </summary>
+ [JsonPropertyName("episode")]
+ public int Episode { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
new file mode 100644
index 000000000..b9844562f
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Headends dto.
+ /// </summary>
+ public class HeadendsDto
+ {
+ /// <summary>
+ /// Gets or sets the headend.
+ /// </summary>
+ [JsonPropertyName("headend")]
+ public string? Headend { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transport.
+ /// </summary>
+ [JsonPropertyName("transport")]
+ public string? Transport { get; set; }
+
+ /// <summary>
+ /// Gets or sets the location.
+ /// </summary>
+ [JsonPropertyName("location")]
+ public string? Location { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of lineups.
+ /// </summary>
+ [JsonPropertyName("lineups")]
+ public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
new file mode 100644
index 000000000..a1ae3ca6d
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs
@@ -0,0 +1,70 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Image data dto.
+ /// </summary>
+ public class ImageDataDto
+ {
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ [JsonPropertyName("width")]
+ public string? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ [JsonPropertyName("height")]
+ public string? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the uri.
+ /// </summary>
+ [JsonPropertyName("uri")]
+ public string? Uri { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ [JsonPropertyName("size")]
+ public string? Size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the aspect.
+ /// </summary>
+ [JsonPropertyName("aspect")]
+ public string? Aspect { get; set; }
+
+ /// <summary>
+ /// Gets or sets the category.
+ /// </summary>
+ [JsonPropertyName("category")]
+ public string? Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the text.
+ /// </summary>
+ [JsonPropertyName("text")]
+ public string? Text { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary.
+ /// </summary>
+ [JsonPropertyName("primary")]
+ public string? Primary { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tier.
+ /// </summary>
+ [JsonPropertyName("tier")]
+ public string? Tier { get; set; }
+
+ /// <summary>
+ /// Gets or sets the caption.
+ /// </summary>
+ [JsonPropertyName("caption")]
+ public CaptionDto? Caption { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
new file mode 100644
index 000000000..3dc64e5d8
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// The lineup dto.
+ /// </summary>
+ public class LineupDto
+ {
+ /// <summary>
+ /// Gets or sets the linup.
+ /// </summary>
+ [JsonPropertyName("lineup")]
+ public string? Lineup { get; set; }
+
+ /// <summary>
+ /// Gets or sets the lineup name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transport.
+ /// </summary>
+ [JsonPropertyName("transport")]
+ public string? Transport { get; set; }
+
+ /// <summary>
+ /// Gets or sets the location.
+ /// </summary>
+ [JsonPropertyName("location")]
+ public string? Location { get; set; }
+
+ /// <summary>
+ /// Gets or sets the uri.
+ /// </summary>
+ [JsonPropertyName("uri")]
+ public string? Uri { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this lineup was deleted.
+ /// </summary>
+ [JsonPropertyName("isDeleted")]
+ public bool? IsDeleted { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
new file mode 100644
index 000000000..f19081781
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Lineups dto.
+ /// </summary>
+ public class LineupsDto
+ {
+ /// <summary>
+ /// Gets or sets the response code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ [JsonPropertyName("serverID")]
+ public string? ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the datetime.
+ /// </summary>
+ [JsonPropertyName("datetime")]
+ public DateTime? LineupTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of lineups.
+ /// </summary>
+ [JsonPropertyName("lineups")]
+ public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
new file mode 100644
index 000000000..fecc55e03
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Logo dto.
+ /// </summary>
+ public class LogoDto
+ {
+ /// <summary>
+ /// Gets or sets the url.
+ /// </summary>
+ [JsonPropertyName("URL")]
+ public string? Url { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ [JsonPropertyName("height")]
+ public int Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ [JsonPropertyName("width")]
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
new file mode 100644
index 000000000..ffd02d474
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
@@ -0,0 +1,58 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Map dto.
+ /// </summary>
+ public class MapDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel.
+ /// </summary>
+ [JsonPropertyName("channel")]
+ public string? Channel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider callsign.
+ /// </summary>
+ [JsonPropertyName("providerCallsign")]
+ public string? ProvderCallsign { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logical channel number.
+ /// </summary>
+ [JsonPropertyName("logicalChannelNumber")]
+ public string? LogicalChannelNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the uhfvhf.
+ /// </summary>
+ [JsonPropertyName("uhfVhf")]
+ public int UhfVhf { get; set; }
+
+ /// <summary>
+ /// Gets or sets the atsc major.
+ /// </summary>
+ [JsonPropertyName("atscMajor")]
+ public int AtscMajor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the atsc minor.
+ /// </summary>
+ [JsonPropertyName("atscMinor")]
+ public int AtscMinor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the match type.
+ /// </summary>
+ [JsonPropertyName("matchType")]
+ public string? MatchType { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
new file mode 100644
index 000000000..40faa493c
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Metadata dto.
+ /// </summary>
+ public class MetadataDto
+ {
+ /// <summary>
+ /// Gets or sets the linup.
+ /// </summary>
+ [JsonPropertyName("lineup")]
+ public string? Lineup { get; set; }
+
+ /// <summary>
+ /// Gets or sets the modified timestamp.
+ /// </summary>
+ [JsonPropertyName("modified")]
+ public string? Modified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transport.
+ /// </summary>
+ [JsonPropertyName("transport")]
+ public string? Transport { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
new file mode 100644
index 000000000..43f290156
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Metadata programs dto.
+ /// </summary>
+ public class MetadataProgramsDto
+ {
+ /// <summary>
+ /// Gets or sets the gracenote object.
+ /// </summary>
+ [JsonPropertyName("Gracenote")]
+ public GracenoteDto? Gracenote { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
new file mode 100644
index 000000000..04560ab55
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Metadata schedule dto.
+ /// </summary>
+ public class MetadataScheduleDto
+ {
+ /// <summary>
+ /// Gets or sets the modified timestamp.
+ /// </summary>
+ [JsonPropertyName("modified")]
+ public string? Modified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start date.
+ /// </summary>
+ [JsonPropertyName("startDate")]
+ public DateTime? StartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end date.
+ /// </summary>
+ [JsonPropertyName("endDate")]
+ public DateTime? EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the days count.
+ /// </summary>
+ [JsonPropertyName("days")]
+ public int Days { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
new file mode 100644
index 000000000..31bef423b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Movie dto.
+ /// </summary>
+ public class MovieDto
+ {
+ /// <summary>
+ /// Gets or sets the year.
+ /// </summary>
+ [JsonPropertyName("year")]
+ public string? Year { get; set; }
+
+ /// <summary>
+ /// Gets or sets the duration.
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public int Duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of quality rating.
+ /// </summary>
+ [JsonPropertyName("qualityRating")]
+ public IReadOnlyList<QualityRatingDto> QualityRating { get; set; } = Array.Empty<QualityRatingDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
new file mode 100644
index 000000000..e8b15dc07
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Multipart dto.
+ /// </summary>
+ public class MultipartDto
+ {
+ /// <summary>
+ /// Gets or sets the part number.
+ /// </summary>
+ [JsonPropertyName("partNumber")]
+ public int PartNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total parts.
+ /// </summary>
+ [JsonPropertyName("totalParts")]
+ public int TotalParts { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
new file mode 100644
index 000000000..84c48f67f
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Program details dto.
+ /// </summary>
+ public class ProgramDetailsDto
+ {
+ /// <summary>
+ /// Gets or sets the audience.
+ /// </summary>
+ [JsonPropertyName("audience")]
+ public string? Audience { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of titles.
+ /// </summary>
+ [JsonPropertyName("titles")]
+ public IReadOnlyList<TitleDto> Titles { get; set; } = Array.Empty<TitleDto>();
+
+ /// <summary>
+ /// Gets or sets the event details object.
+ /// </summary>
+ [JsonPropertyName("eventDetails")]
+ public EventDetailsDto? EventDetails { get; set; }
+
+ /// <summary>
+ /// Gets or sets the descriptions.
+ /// </summary>
+ [JsonPropertyName("descriptions")]
+ public DescriptionsProgramDto? Descriptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the original air date.
+ /// </summary>
+ [JsonPropertyName("originalAirDate")]
+ public DateTime? OriginalAirDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of genres.
+ /// </summary>
+ [JsonPropertyName("genres")]
+ public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ [JsonPropertyName("episodeTitle150")]
+ public string? EpisodeTitle150 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of metadata.
+ /// </summary>
+ [JsonPropertyName("metadata")]
+ public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
+
+ /// <summary>
+ /// Gets or sets the list of content raitings.
+ /// </summary>
+ [JsonPropertyName("contentRating")]
+ public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
+
+ /// <summary>
+ /// Gets or sets the list of cast.
+ /// </summary>
+ [JsonPropertyName("cast")]
+ public IReadOnlyList<CastDto> Cast { get; set; } = Array.Empty<CastDto>();
+
+ /// <summary>
+ /// Gets or sets the list of crew.
+ /// </summary>
+ [JsonPropertyName("crew")]
+ public IReadOnlyList<CrewDto> Crew { get; set; } = Array.Empty<CrewDto>();
+
+ /// <summary>
+ /// Gets or sets the entity type.
+ /// </summary>
+ [JsonPropertyName("entityType")]
+ public string? EntityType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the show type.
+ /// </summary>
+ [JsonPropertyName("showType")]
+ public string? ShowType { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether there is image artwork.
+ /// </summary>
+ [JsonPropertyName("hasImageArtwork")]
+ public bool HasImageArtwork { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image.
+ /// </summary>
+ [JsonPropertyName("primaryImage")]
+ public string? PrimaryImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the thumb image.
+ /// </summary>
+ [JsonPropertyName("thumbImage")]
+ public string? ThumbImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backdrop image.
+ /// </summary>
+ [JsonPropertyName("backdropImage")]
+ public string? BackdropImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the banner image.
+ /// </summary>
+ [JsonPropertyName("bannerImage")]
+ public string? BannerImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image id.
+ /// </summary>
+ [JsonPropertyName("imageID")]
+ public string? ImageId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of content advisory.
+ /// </summary>
+ [JsonPropertyName("contentAdvisory")]
+ public IReadOnlyList<string> ContentAdvisory { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the movie object.
+ /// </summary>
+ [JsonPropertyName("movie")]
+ public MovieDto? Movie { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of recommendations.
+ /// </summary>
+ [JsonPropertyName("recommendations")]
+ public IReadOnlyList<RecommendationDto> Recommendations { get; set; } = Array.Empty<RecommendationDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
new file mode 100644
index 000000000..60389b45b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Program dto.
+ /// </summary>
+ public class ProgramDto
+ {
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the air date time.
+ /// </summary>
+ [JsonPropertyName("airDateTime")]
+ public DateTime? AirDateTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the duration.
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public int Duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the md5.
+ /// </summary>
+ [JsonPropertyName("md5")]
+ public string? Md5 { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of audio properties.
+ /// </summary>
+ [JsonPropertyName("audioProperties")]
+ public IReadOnlyList<string> AudioProperties { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the list of video properties.
+ /// </summary>
+ [JsonPropertyName("videoProperties")]
+ public IReadOnlyList<string> VideoProperties { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the list of ratings.
+ /// </summary>
+ [JsonPropertyName("ratings")]
+ public IReadOnlyList<RatingDto> Ratings { get; set; } = Array.Empty<RatingDto>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this program is new.
+ /// </summary>
+ [JsonPropertyName("new")]
+ public bool? New { get; set; }
+
+ /// <summary>
+ /// Gets or sets the multipart object.
+ /// </summary>
+ [JsonPropertyName("multipart")]
+ public MultipartDto? Multipart { get; set; }
+
+ /// <summary>
+ /// Gets or sets the live tape delay.
+ /// </summary>
+ [JsonPropertyName("liveTapeDelay")]
+ public string? LiveTapeDelay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this is the premiere.
+ /// </summary>
+ [JsonPropertyName("premiere")]
+ public bool Premiere { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this is a repeat.
+ /// </summary>
+ [JsonPropertyName("repeat")]
+ public bool Repeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the premiere or finale.
+ /// </summary>
+ [JsonPropertyName("isPremiereOrFinale")]
+ public string? IsPremiereOrFinale { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
new file mode 100644
index 000000000..c5ddcf7c5
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs
@@ -0,0 +1,40 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Quality rating dto.
+ /// </summary>
+ public class QualityRatingDto
+ {
+ /// <summary>
+ /// Gets or sets the ratings body.
+ /// </summary>
+ [JsonPropertyName("ratingsBody")]
+ public string? RatingsBody { get; set; }
+
+ /// <summary>
+ /// Gets or sets the rating.
+ /// </summary>
+ [JsonPropertyName("rating")]
+ public string? Rating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the min rating.
+ /// </summary>
+ [JsonPropertyName("minRating")]
+ public string? MinRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max rating.
+ /// </summary>
+ [JsonPropertyName("maxRating")]
+ public string? MaxRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the increment.
+ /// </summary>
+ [JsonPropertyName("increment")]
+ public string? Increment { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
new file mode 100644
index 000000000..e04b619a4
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Rating dto.
+ /// </summary>
+ public class RatingDto
+ {
+ /// <summary>
+ /// Gets or sets the body.
+ /// </summary>
+ [JsonPropertyName("body")]
+ public string? Body { get; set; }
+
+ /// <summary>
+ /// Gets or sets the code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
new file mode 100644
index 000000000..c8f79fd1c
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Recommendation dto.
+ /// </summary>
+ public class RecommendationDto
+ {
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ [JsonPropertyName("title120")]
+ public string? Title120 { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
new file mode 100644
index 000000000..0cd05709b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Request schedule for channel dto.
+ /// </summary>
+ public class RequestScheduleForChannelDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of dates.
+ /// </summary>
+ [JsonPropertyName("date")]
+ public IReadOnlyList<string> Date { get; set; } = Array.Empty<string>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
new file mode 100644
index 000000000..84e224b71
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Show image dto.
+ /// </summary>
+ public class ShowImagesDto
+ {
+ /// <summary>
+ /// Gets or sets the program id.
+ /// </summary>
+ [JsonPropertyName("programID")]
+ public string? ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of data.
+ /// </summary>
+ [JsonPropertyName("data")]
+ public IReadOnlyList<ImageDataDto> Data { get; set; } = Array.Empty<ImageDataDto>();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
new file mode 100644
index 000000000..d797fd49b
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Station dto.
+ /// </summary>
+ public class StationDto
+ {
+ /// <summary>
+ /// Gets or sets the station id.
+ /// </summary>
+ [JsonPropertyName("stationID")]
+ public string? StationId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the callsign.
+ /// </summary>
+ [JsonPropertyName("callsign")]
+ public string? Callsign { get; set; }
+
+ /// <summary>
+ /// Gets or sets the broadcast language.
+ /// </summary>
+ [JsonPropertyName("broadcastLanguage")]
+ public IReadOnlyList<string> BroadcastLanguage { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the description language.
+ /// </summary>
+ [JsonPropertyName("descriptionLanguage")]
+ public IReadOnlyList<string> DescriptionLanguage { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets the broadcaster.
+ /// </summary>
+ [JsonPropertyName("broadcaster")]
+ public BroadcasterDto? Broadcaster { get; set; }
+
+ /// <summary>
+ /// Gets or sets the affiliate.
+ /// </summary>
+ [JsonPropertyName("affiliate")]
+ public string? Affiliate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logo.
+ /// </summary>
+ [JsonPropertyName("logo")]
+ public LogoDto? Logo { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether it is commercial free.
+ /// </summary>
+ [JsonPropertyName("isCommercialFree")]
+ public bool? IsCommercialFree { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
new file mode 100644
index 000000000..61cd4a9b0
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// Title dto.
+ /// </summary>
+ public class TitleDto
+ {
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ [JsonPropertyName("title120")]
+ public string? Title120 { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
new file mode 100644
index 000000000..afb999486
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
+{
+ /// <summary>
+ /// The token dto.
+ /// </summary>
+ public class TokenDto
+ {
+ /// <summary>
+ /// Gets or sets the response code.
+ /// </summary>
+ [JsonPropertyName("code")]
+ public int Code { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response message.
+ /// </summary>
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ [JsonPropertyName("serverID")]
+ public string? ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the token.
+ /// </summary>
+ [JsonPropertyName("token")]
+ public string? Token { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current datetime.
+ /// </summary>
+ [JsonPropertyName("datetime")]
+ public DateTime? TokenTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response message.
+ /// </summary>
+ [JsonPropertyName("response")]
+ public string? Response { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index ebad4eddf..3da9d02b8 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using Jellyfin.XmlTv;
using Jellyfin.XmlTv.Entities;
using MediaBrowser.Common.Extensions;
@@ -59,41 +60,41 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return _config.Configuration.PreferredMetadataLanguage;
}
- private async Task<string> GetXml(string path, CancellationToken cancellationToken)
+ private async Task<string> GetXml(ListingsProviderInfo info, CancellationToken cancellationToken)
{
- _logger.LogInformation("xmltv path: {Path}", path);
+ _logger.LogInformation("xmltv path: {Path}", info.Path);
- if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- return UnzipIfNeeded(path, path);
+ return UnzipIfNeeded(info.Path, info.Path);
}
- string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml";
+ string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + "-" + info.Id + ".xml";
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
if (File.Exists(cacheFile))
{
- return UnzipIfNeeded(path, cacheFile);
+ return UnzipIfNeeded(info.Path, cacheFile);
}
- _logger.LogInformation("Downloading xmltv listings from {Path}", path);
+ _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
+ await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous))
{
await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
- return UnzipIfNeeded(path, cacheFile);
+ return UnzipIfNeeded(info.Path, cacheFile);
}
- private string UnzipIfNeeded(string originalUrl, string file)
+ private string UnzipIfNeeded(ReadOnlySpan<char> originalUrl, string file)
{
- string ext = Path.GetExtension(originalUrl.Split('?')[0]);
+ ReadOnlySpan<char> ext = Path.GetExtension(originalUrl.LeftPart('?'));
- if (string.Equals(ext, ".gz", StringComparison.OrdinalIgnoreCase))
+ if (ext.Equals(".gz", StringComparison.OrdinalIgnoreCase))
{
try
{
@@ -162,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogDebug("Getting xmltv programs for channel {Id}", channelId);
- string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
+ string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info));
@@ -189,10 +190,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
IsSeries = program.Episode != null,
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
IsPremiere = program.Premiere != null,
- IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
- IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
- IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
- IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
+ IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+ IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
ImageUrl = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source) ? program.Icon.Source : null,
HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source),
OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null,
@@ -256,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location)
{
// In theory this should never be called because there is always only one lineup
- string path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false);
+ string path = await GetXml(info, CancellationToken.None).ConfigureAwait(false);
_logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info));
IEnumerable<XmlTvChannel> results = reader.GetChannels();
@@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken)
{
// In theory this should never be called because there is always only one lineup
- string path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false);
+ string path = await GetXml(info, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Opening XmlTvReader for {Path}", path);
var reader = new XmlTvReader(path, GetLanguage(info));
var results = reader.GetChannels();
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 21e1409ac..323b96021 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -7,12 +7,12 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
@@ -161,7 +161,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(Series) },
+ IncludeItemTypes = new[] { BaseItemKind.Series },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
@@ -255,7 +255,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(Series) },
+ IncludeItemTypes = new[] { BaseItemKind.Series },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -298,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(Series) },
+ IncludeItemTypes = new[] { BaseItemKind.Series },
Name = seriesName,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
@@ -309,7 +309,7 @@ namespace Emby.Server.Implementations.LiveTv
{
program = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ExternalSeriesId = programSeriesId,
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
@@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error getting image info for {name}", info.Name);
+ _logger.LogError(ex, "Error getting image info for {Name}", info.Name);
}
return null;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index d964769b5..047d8e98c 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -33,8 +33,6 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace Emby.Server.Implementations.LiveTv
{
@@ -65,6 +63,8 @@ namespace Emby.Server.Implementations.LiveTv
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
+ private bool _disposed = false;
+
public LiveTvManager(
IServerConfigurationManager config,
ILogger<LiveTvManager> logger,
@@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids,
IsSports = query.IsSports,
IsSeries = query.IsSeries,
- IncludeItemTypes = new[] { nameof(LiveTvChannel) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked,
@@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.LiveTv
// Set the total bitrate if not already supplied
mediaSource.InferTotalBitrate();
- if (!(service is EmbyTV.EmbyTV))
+ if (service is not EmbyTV.EmbyTV)
{
// We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
// mediaSource.SupportsDirectPlay = false;
@@ -520,7 +520,7 @@ namespace Emby.Server.Implementations.LiveTv
return item;
}
- private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
+ private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel)
{
var id = _tvDtoService.GetInternalProgramId(info.Id);
@@ -559,8 +559,6 @@ namespace Emby.Server.Implementations.LiveTv
item.ParentId = channel.Id;
- // item.ChannelType = channelType;
-
item.Audio = info.Audio;
item.ChannelId = channel.Id;
item.CommunityRating ??= info.CommunityRating;
@@ -772,7 +770,7 @@ namespace Emby.Server.Implementations.LiveTv
item.OnMetadataChanged();
}
- return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated);
+ return (item, isNew, isUpdated);
}
public async Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null)
@@ -810,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate,
@@ -874,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
IsAiring = query.IsAiring,
HasAired = query.HasAired,
IsNews = query.IsNews,
@@ -1054,7 +1052,7 @@ namespace Emby.Server.Implementations.LiveTv
{
cancellationToken.ThrowIfCancellationRequested();
- _logger.LogDebug("Refreshing guide from {name}", service.Name);
+ _logger.LogDebug("Refreshing guide from {Name}", service.Name);
try
{
@@ -1085,8 +1083,8 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase)
{
- CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
- CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
+ CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken);
+ CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken);
}
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@@ -1135,7 +1133,7 @@ namespace Emby.Server.Implementations.LiveTv
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name);
+ _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
}
numComplete++;
@@ -1177,7 +1175,7 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@@ -1187,14 +1185,14 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var program in channelPrograms)
{
- var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
- var programItem = programTuple.Item1;
+ var programTuple = GetProgram(program, existingPrograms, currentChannel);
+ var programItem = programTuple.item;
- if (programTuple.Item2)
+ if (programTuple.isNew)
{
newPrograms.Add(programItem);
}
- else if (programTuple.Item3)
+ else if (programTuple.isUpdated)
{
updatedPrograms.Add(programItem);
}
@@ -1248,7 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name);
+ _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
}
numComplete++;
@@ -1261,7 +1259,7 @@ namespace Emby.Server.Implementations.LiveTv
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
}
- private void CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
+ private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
{
var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
{
@@ -1328,25 +1326,25 @@ namespace Emby.Server.Implementations.LiveTv
.Select(i => i.Id)
.ToList();
- var excludeItemTypes = new List<string>();
+ var excludeItemTypes = new List<BaseItemKind>();
if (folderIds.Count == 0)
{
return new QueryResult<BaseItem>();
}
- var includeItemTypes = new List<string>();
+ var includeItemTypes = new List<BaseItemKind>();
var genres = new List<string>();
if (query.IsMovie.HasValue)
{
if (query.IsMovie.Value)
{
- includeItemTypes.Add(nameof(Movie));
+ includeItemTypes.Add(BaseItemKind.Movie);
}
else
{
- excludeItemTypes.Add(nameof(Movie));
+ excludeItemTypes.Add(BaseItemKind.Movie);
}
}
@@ -1354,11 +1352,11 @@ namespace Emby.Server.Implementations.LiveTv
{
if (query.IsSeries.Value)
{
- includeItemTypes.Add(nameof(Episode));
+ includeItemTypes.Add(BaseItemKind.Episode);
}
else
{
- excludeItemTypes.Add(nameof(Episode));
+ excludeItemTypes.Add(BaseItemKind.Episode);
}
}
@@ -1385,10 +1383,10 @@ namespace Emby.Server.Implementations.LiveTv
// var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
// return new QueryResult<BaseItem>
- //{
+ // {
// Items = items,
// TotalRecordCount = items.Length
- //};
+ // };
dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray();
}
@@ -1425,16 +1423,15 @@ namespace Emby.Server.Implementations.LiveTv
return result;
}
- public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList<ItemFields> fields, User user = null)
+ public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null)
{
var programTuples = new List<Tuple<BaseItemDto, string, string>>();
var hasChannelImage = fields.Contains(ItemFields.ChannelImage);
var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo);
- foreach (var tuple in tuples)
+ foreach (var (item, dto) in programs)
{
- var program = (LiveTvProgram)tuple.Item1;
- var dto = tuple.Item2;
+ var program = (LiveTvProgram)item;
dto.StartDate = program.StartDate;
dto.EpisodeTitle = program.EpisodeTitle;
@@ -1724,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv
await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
- if (!(service is EmbyTV.EmbyTV))
+ if (service is not EmbyTV.EmbyTV)
{
TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
}
@@ -1871,15 +1868,15 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetItemById(internalChannelId);
}
- public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> tuples, DtoOptions options, User user)
+ public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user)
{
var now = DateTime.UtcNow;
- var channelIds = tuples.Select(i => i.Item2.Id).Distinct().ToArray();
+ var channelIds = items.Select(i => i.Item2.Id).Distinct().ToArray();
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ChannelIds = channelIds,
MaxStartDate = now,
MinEndDate = now,
@@ -1896,7 +1893,7 @@ namespace Emby.Server.Implementations.LiveTv
var addCurrentProgram = options.AddCurrentProgram;
- foreach (var tuple in tuples)
+ foreach (var tuple in items)
{
var dto = tuple.Item1;
var channel = tuple.Item2;
@@ -2050,7 +2047,7 @@ namespace Emby.Server.Implementations.LiveTv
_logger.LogInformation("New recording scheduled");
- if (!(service is EmbyTV.EmbyTV))
+ if (service is not EmbyTV.EmbyTV)
{
TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
new TimerEventInfo(newTimerId)
@@ -2118,17 +2115,13 @@ namespace Emby.Server.Implementations.LiveTv
};
}
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
+ /// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
- private bool _disposed = false;
-
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -2324,20 +2317,20 @@ namespace Emby.Server.Implementations.LiveTv
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
- public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
+ public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber)
{
var config = GetConfiguration();
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
- listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelId, StringComparison.OrdinalIgnoreCase)).ToArray();
+ listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
- if (!string.Equals(tunerChannelId, providerChannelId, StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{
var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair
{
- Name = tunerChannelId,
- Value = providerChannelId
+ Name = tunerChannelNumber,
+ Value = providerChannelNumber
});
listingsProviderInfo.ChannelMappings = list.ToArray();
}
@@ -2357,10 +2350,10 @@ namespace Emby.Server.Implementations.LiveTv
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
- return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
+ return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase));
}
- public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> epgChannels)
+ public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels)
{
var result = new TunerChannelMapping
{
@@ -2373,7 +2366,7 @@ namespace Emby.Server.Implementations.LiveTv
result.Name = tunerChannel.Number + " " + result.Name;
}
- var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, epgChannels);
+ var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels);
if (providerChannel != null)
{
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index ecd28097d..4b7584af3 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.LiveTv
// Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
{
- source.Path = _appHost.GetSmartApiUrl(string.Empty);
+ source.Path = _appHost.GetApiUrlForLocalAccess();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 5941613cf..2b82f2462 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -23,10 +23,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
- protected readonly IServerConfigurationManager Config;
- protected readonly ILogger<BaseTunerHost> Logger;
- protected readonly IFileSystem FileSystem;
-
private readonly IMemoryCache _memoryCache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
@@ -37,12 +33,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
FileSystem = fileSystem;
}
- public virtual bool IsSupported => true;
+ protected IServerConfigurationManager Config { get; }
- protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+ protected ILogger<BaseTunerHost> Logger { get; }
+
+ protected IFileSystem FileSystem { get; }
+
+ public virtual bool IsSupported => true;
public abstract string Type { get; }
+ protected virtual string ChannelIdPrefix => Type + "_";
+
+ protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
+
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
var key = tuner.Id;
@@ -92,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
- await using var writeStream = File.OpenWrite(channelCacheFile);
+ await using var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
@@ -108,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- await using var readStream = File.OpenRead(channelCacheFile);
+ await using var readStream = AsyncFile.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
@@ -158,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return new List<MediaSourceInfo>();
}
- protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tuner, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
+ protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
@@ -217,8 +221,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
throw new LiveTvConflictException();
}
- protected virtual string ChannelIdPrefix => Type + "_";
-
protected virtual bool IsValidChannelId(string channelId)
{
if (string.IsNullOrEmpty(channelId))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
new file mode 100644
index 000000000..069b4fab6
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs
@@ -0,0 +1,35 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class HdHomerunChannelCommands : IHdHomerunChannelCommands
+ {
+ private string? _channel;
+ private string? _profile;
+
+ public HdHomerunChannelCommands(string? channel, string? profile)
+ {
+ _channel = channel;
+ _profile = profile;
+ }
+
+ public IEnumerable<(string, string)> GetCommands()
+ {
+ if (!string.IsNullOrEmpty(_channel))
+ {
+ if (!string.IsNullOrEmpty(_profile)
+ && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
+ {
+ yield return ("vchannel", $"{_channel} transcode={_profile}");
+ }
+ else
+ {
+ yield return ("vchannel", _channel);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 54de841fe..78ea7bd0f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -12,8 +12,9 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -35,7 +36,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
- private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions;
@@ -49,7 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- INetworkManager networkManager,
IStreamHelper streamHelper,
IMemoryCache memoryCache)
: base(config, logger, fileSystem, memoryCache)
@@ -57,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
- _networkManager = networkManager;
_streamHelper = streamHelper;
_jsonOptions = JsonDefaults.Options;
@@ -69,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override string ChannelIdPrefix => "hdhr_";
- private string GetChannelId(TunerHostInfo info, Channels i)
+ private string GetChannelId(Channels i)
=> ChannelIdPrefix + i.GuideNumber;
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
@@ -89,22 +87,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return lineup.Where(i => !i.DRM).ToList();
}
- private class HdHomerunChannelInfo : ChannelInfo
- {
- public bool IsLegacyTuner { get; set; }
- }
-
- protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
- var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false);
+ var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
return lineup.Select(i => new HdHomerunChannelInfo
{
Name = i.GuideName,
Number = i.GuideNumber,
- Id = GetChannelId(info, i),
+ Id = GetChannelId(i),
IsFavorite = i.Favorite,
- TunerHostId = info.Id,
+ TunerHostId = tuner.Id,
IsHD = i.HD,
AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec,
@@ -254,7 +247,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- var tuners = new List<LiveTvTunerInfo>();
+ var tuners = new List<LiveTvTunerInfo>(model.TunerCount);
var uri = new Uri(GetApiUrl(info));
@@ -263,10 +256,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Legacy HdHomeruns are IPv4 only
var ipInfo = IPAddress.Parse(uri.Host);
- for (int i = 0; i < model.TunerCount; ++i)
+ 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 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
@@ -454,28 +447,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Path = url,
Protocol = MediaProtocol.Udp,
MediaStreams = new List<MediaStream>
- {
- new MediaStream
- {
- Type = MediaStreamType.Video,
- // Set the index to -1 because we don't know the exact index of the video stream within the container
- Index = -1,
- IsInterlaced = isInterlaced,
- Codec = videoCodec,
- Width = width,
- Height = height,
- BitRate = videoBitrate,
- NalLengthSize = nal
- },
- new MediaStream
- {
- Type = MediaStreamType.Audio,
- // Set the index to -1 because we don't know the exact index of the audio stream within the container
- Index = -1,
- Codec = audioCodec,
- BitRate = audioBitrate
- }
- },
+ {
+ new MediaStream
+ {
+ Type = MediaStreamType.Video,
+ // Set the index to -1 because we don't know the exact index of the video stream within the container
+ Index = -1,
+ IsInterlaced = isInterlaced,
+ Codec = videoCodec,
+ Width = width,
+ Height = height,
+ BitRate = videoBitrate,
+ NalLengthSize = nal
+ },
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ // Set the index to -1 because we don't know the exact index of the audio stream within the container
+ Index = -1,
+ Codec = audioCodec,
+ BitRate = audioBitrate
+ }
+ },
RequiresOpening = true,
RequiresClosing = true,
BufferMs = 0,
@@ -495,57 +488,53 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return mediaSource;
}
- protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
+ protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{
var list = new List<MediaSourceInfo>();
- var channelId = channelInfo.Id;
+ var channelId = channel.Id;
var hdhrId = GetHdHrIdFromChannelId(channelId);
- var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo;
-
- var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner;
-
- if (isLegacyTuner)
+ if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner)
{
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
else
{
- var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+ var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false);
if (modelInfo != null && modelInfo.SupportsTranscoding)
{
- if (info.AllowHWTranscoding)
+ if (tuner.AllowHWTranscoding)
{
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240"));
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile"));
}
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
if (list.Count == 0)
{
- list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
+ list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
}
}
return list;
}
- protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
+ protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- var tunerCount = info.TunerCount;
+ var tunerCount = tunerHost.TunerCount;
if (tunerCount > 0)
{
- var tunerHostId = info.Id;
+ var tunerHostId = tunerHost.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count() >= tunerCount)
@@ -554,28 +543,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- var profile = streamId.Split('_')[0];
+ var profile = streamId.AsSpan().LeftPart('_').ToString();
- Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile);
+ Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile);
- var hdhrId = GetHdHrIdFromChannelId(channelInfo.Id);
+ var hdhrId = GetHdHrIdFromChannelId(channel.Id);
- var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
+ var hdhomerunChannel = channel as HdHomerunChannelInfo;
- var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
+ var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false);
if (!modelInfo.SupportsTranscoding)
{
profile = "native";
}
- var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
+ var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile);
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
{
return new HdHomerunUdpStream(
mediaSource,
- info,
+ tunerHost,
streamId,
new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
modelInfo.TunerCount,
@@ -591,7 +580,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
mediaSource.Protocol = MediaProtocol.Http;
- var httpUrl = channelInfo.Path;
+ var httpUrl = channel.Path;
// If raw was used, the tuner doesn't support params
if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
@@ -603,7 +592,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new SharedHttpStream(
mediaSource,
- info,
+ tunerHost,
streamId,
FileSystem,
_httpClientFactory,
@@ -615,7 +604,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new HdHomerunUdpStream(
mediaSource,
- info,
+ tunerHost,
streamId,
new HdHomerunChannelCommands(hdhomerunChannel.Number, profile),
modelInfo.TunerCount,
@@ -721,5 +710,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return hostInfo;
}
+
+ private class HdHomerunChannelInfo : ChannelInfo
+ {
+ public bool IsLegacyTuner { get; set; }
+ }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 3016eeda2..9ab4cc628 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -5,12 +5,10 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
-using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -18,69 +16,6 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
- public interface IHdHomerunChannelCommands
- {
- IEnumerable<(string, string)> GetCommands();
- }
-
- public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
- {
- private string _channel;
- private string _program;
- public LegacyHdHomerunChannelCommands(string url)
- {
- // parse url for channel and program
- var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
- var match = regExp.Match(url);
- if (match.Success)
- {
- _channel = match.Groups[1].Value;
- _program = match.Groups[2].Value;
- }
- }
-
- public IEnumerable<(string, string)> GetCommands()
- {
- if (!string.IsNullOrEmpty(_channel))
- {
- yield return ("channel", _channel);
- }
-
- if (!string.IsNullOrEmpty(_program))
- {
- yield return ("program", _program);
- }
- }
- }
-
- public class HdHomerunChannelCommands : IHdHomerunChannelCommands
- {
- private string _channel;
- private string _profile;
-
- public HdHomerunChannelCommands(string channel, string profile)
- {
- _channel = channel;
- _profile = profile;
- }
-
- public IEnumerable<(string, string)> GetCommands()
- {
- if (!string.IsNullOrEmpty(_channel))
- {
- if (!string.IsNullOrEmpty(_profile)
- && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
- {
- yield return ("vchannel", $"{_channel} transcode={_profile}");
- }
- else
- {
- yield return ("vchannel", _channel);
- }
- }
- }
- }
-
public sealed class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;
@@ -116,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
{
using var client = new TcpClient();
- client.Connect(remoteIp, HdHomeRunPort);
+ await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false);
using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
@@ -149,8 +84,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
if (!_lockkey.HasValue)
{
- var rand = new Random();
- _lockkey = (uint)rand.Next();
+ _lockkey = (uint)Random.Shared.Next();
}
var lockKeyValue = _lockkey.Value;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index df3460212..d2f033439 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
- Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host);
+ Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host);
var remoteAddress = IPAddress.Parse(uri.Host);
IPAddress localAddress;
@@ -101,7 +100,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- if (localAddress.IsIPv4MappedToIPv6) {
+ if (localAddress.IsIPv4MappedToIPv6)
+ {
localAddress = localAddress.MapToIPv4();
}
@@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// OpenedMediaSource.Path = tempFile;
// OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
// OpenedMediaSource.SupportsDirectPlay = false;
// OpenedMediaSource.SupportsDirectStream = true;
@@ -156,11 +156,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false);
}
- public string GetFilePath()
- {
- return TempFilePath;
- }
-
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
using (udpClient)
@@ -184,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = false;
}
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
}
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@@ -201,7 +196,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
cancellationToken,
timeOutSource.Token))
{
- var resTask = udpClient.ReceiveAsync();
+ var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
{
resTask.Dispose();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
new file mode 100644
index 000000000..153354932
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs
@@ -0,0 +1,11 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public interface IHdHomerunChannelCommands
+ {
+ IEnumerable<(string, string)> GetCommands();
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
new file mode 100644
index 000000000..26627b8aa
--- /dev/null
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
+{
+ public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
+ {
+ private string? _channel;
+ private string? _program;
+
+ public LegacyHdHomerunChannelCommands(string url)
+ {
+ // parse url for channel and program
+ var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
+ var match = regExp.Match(url);
+ if (match.Success)
+ {
+ _channel = match.Groups[1].Value;
+ _program = match.Groups[2].Value;
+ }
+ }
+
+ public IEnumerable<(string, string)> GetCommands()
+ {
+ if (!string.IsNullOrEmpty(_channel))
+ {
+ yield return ("channel", _channel);
+ }
+
+ if (!string.IsNullOrEmpty(_program))
+ {
+ yield return ("program", _program);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 96a678c1d..5581ba87c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -3,10 +3,8 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -22,14 +20,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
private readonly IConfigurationManager _configurationManager;
- protected readonly IFileSystem FileSystem;
-
- protected readonly IStreamHelper StreamHelper;
-
- protected string TempFilePath;
- protected readonly ILogger Logger;
- protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
-
public LiveStream(
MediaSourceInfo mediaSource,
TunerHostInfo tuner,
@@ -57,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
SetTempFilePath("ts");
}
- protected virtual int EmptyReadLimit => 1000;
+ protected IFileSystem FileSystem { get; }
+
+ protected IStreamHelper StreamHelper { get; }
+
+ protected ILogger Logger { get; }
+
+ protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource();
+
+ protected string TempFilePath { get; set; }
public MediaSourceInfo OriginalMediaSource { get; set; }
@@ -97,123 +95,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.CompletedTask;
}
- protected FileStream GetInputStream(string path, bool allowAsyncFileRead)
+ public Stream GetStream()
+ {
+ var stream = GetInputStream(TempFilePath);
+ bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
+ if (seekFile)
+ {
+ TrySeek(stream, -20000);
+ }
+
+ return stream;
+ }
+
+ protected FileStream GetInputStream(string path)
=> new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan);
-
- public Task DeleteTempFiles()
- {
- return DeleteTempFiles(GetStreamFilePaths());
- }
+ FileOptions.SequentialScan | FileOptions.Asynchronous);
- protected async Task DeleteTempFiles(IEnumerable<string> paths, int retryCount = 0)
+ protected async Task DeleteTempFiles(string path, int retryCount = 0)
{
if (retryCount == 0)
{
- Logger.LogInformation("Deleting temp files {0}", paths);
+ Logger.LogInformation("Deleting temp file {FilePath}", path);
}
- var failedFiles = new List<string>();
-
- foreach (var path in paths)
- {
- if (!File.Exists(path))
- {
- continue;
- }
-
- try
- {
- FileSystem.DeleteFile(path);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error deleting file {path}", path);
- failedFiles.Add(path);
- }
- }
-
- if (failedFiles.Count > 0 && retryCount <= 40)
- {
- await Task.Delay(500).ConfigureAwait(false);
- await DeleteTempFiles(failedFiles, retryCount + 1).ConfigureAwait(false);
- }
- }
-
- protected virtual List<string> GetStreamFilePaths()
- {
- return new List<string> { TempFilePath };
- }
-
- public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
- {
- using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
- cancellationToken = linkedCancellationTokenSource.Token;
-
- // use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
- var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;
-
- bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10;
-
- var nextFileInfo = GetNextFile(null);
- var nextFile = nextFileInfo.file;
- var isLastFile = nextFileInfo.isLastFile;
-
- while (!string.IsNullOrEmpty(nextFile))
- {
- var emptyReadLimit = isLastFile ? EmptyReadLimit : 1;
-
- await CopyFile(nextFile, seekFile, emptyReadLimit, allowAsync, stream, cancellationToken).ConfigureAwait(false);
-
- seekFile = false;
- nextFileInfo = GetNextFile(nextFile);
- nextFile = nextFileInfo.file;
- isLastFile = nextFileInfo.isLastFile;
- }
-
- Logger.LogInformation("Live Stream ended.");
- }
-
- private (string file, bool isLastFile) GetNextFile(string currentFile)
- {
- var files = GetStreamFilePaths();
-
- if (string.IsNullOrEmpty(currentFile))
+ try
{
- return (files[^1], true);
+ FileSystem.DeleteFile(path);
}
-
- var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1;
-
- var isLastFile = nextIndex == files.Count - 1;
-
- return (files.ElementAtOrDefault(nextIndex), isLastFile);
- }
-
- private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken)
- {
- using (var inputStream = GetInputStream(path, allowAsync))
+ catch (Exception ex)
{
- if (seekFile)
+ Logger.LogError(ex, "Error deleting file {FilePath}", path);
+ if (retryCount <= 40)
{
- TrySeek(inputStream, -20000);
+ await Task.Delay(500).ConfigureAwait(false);
+ await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false);
}
-
- await StreamHelper.CopyToAsync(
- inputStream,
- stream,
- IODefaults.CopyToBufferSize,
- emptyReadLimit,
- cancellationToken).ConfigureAwait(false);
}
}
- private void TrySeek(FileStream stream, long offset)
+ private void TrySeek(Stream stream, long offset)
{
if (!stream.CanSeek)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 8fa6f5ad6..99486f25c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -71,12 +72,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
- protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
+ protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
{
- var channelIdPrefix = GetFullChannelIdPrefix(info);
+ var channelIdPrefix = GetFullChannelIdPrefix(tuner);
return await new M3uParser(Logger, _httpClientFactory)
- .Parse(info, channelIdPrefix, cancellationToken)
+ .Parse(tuner, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -96,13 +97,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
+ protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- var tunerCount = info.TunerCount;
+ var tunerCount = tunerHost.TunerCount;
if (tunerCount > 0)
{
- var tunerHostId = info.Id;
+ var tunerHostId = tunerHost.Id;
var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
if (liveStreams.Count() >= tunerCount)
@@ -111,7 +112,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
- var sources = await GetChannelStreamMediaSources(info, channelInfo, cancellationToken).ConfigureAwait(false);
+ var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false);
var mediaSource = sources[0];
@@ -119,13 +120,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
- if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+ if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
- return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
+ return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}
- return new LiveStream(mediaSource, info, FileSystem, Logger, Config, _streamHelper);
+ return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper);
}
public async Task Validate(TunerHostInfo info)
@@ -135,9 +136,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
- protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, ChannelInfo channelInfo, CancellationToken cancellationToken)
+ protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken)
{
- return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(info, channelInfo) });
+ return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) });
}
protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 40a162890..708ff52d7 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -10,10 +10,11 @@ using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
@@ -43,22 +44,29 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{
- if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ if (info == null)
{
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
- if (!string.IsNullOrEmpty(info.UserAgent))
- {
- requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
- }
+ throw new ArgumentNullException(nameof(info));
+ }
+
+ if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return AsyncFile.OpenRead(info.Url);
+ }
- var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .SendAsync(requestMessage, cancellationToken)
- .ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
+ if (!string.IsNullOrEmpty(info.UserAgent))
+ {
+ requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
}
- return File.OpenRead(info.Url);
+ // Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files
+ var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadAsStreamAsync(cancellationToken);
}
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
@@ -82,7 +90,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
{
extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
- _logger.LogInformation("Found m3u channel: {0}", extInf);
}
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
@@ -98,6 +105,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.Path = trimmedLine;
channels.Add(channel);
+ _logger.LogInformation("Parsed channel: {ChannelName}", channel.Name);
extInf = string.Empty;
}
}
@@ -230,7 +238,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString();
if (!IsValidChannelNumber(numberString))
{
@@ -275,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// #EXTINF:0,84.0 - VOX Schweiz
if (!string.IsNullOrWhiteSpace(nameInExtInf))
{
- var numberIndex = nameInExtInf.IndexOf(' ');
+ var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal);
if (numberIndex > 0)
{
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
@@ -288,11 +296,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
- attributes.TryGetValue("tvg-name", out string name);
+ string name = nameInExtInf;
if (string.IsNullOrWhiteSpace(name))
{
- name = nameInExtInf;
+ attributes.TryGetValue("tvg-name", out name);
}
if (string.IsNullOrWhiteSpace(name))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index f572151b8..b1ce7b2b3 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
@@ -55,39 +54,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
var typeName = GetType().Name;
- Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
+ Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
// Response stream is disposed manually.
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
- var extension = "ts";
- var requiresRemux = false;
-
var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
- if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
- else if (contentType.IndexOf("mp4", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("dash", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("mpegURL", StringComparison.OrdinalIgnoreCase) != -1 ||
- contentType.IndexOf("text/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- requiresRemux = true;
- }
-
- // Close the stream without any sharing features
- if (requiresRemux)
+ if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
+ || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
{
- using (response)
- {
- return;
- }
+ // Close the stream without any sharing features
+ response.Dispose();
+ return;
}
- SetTempFilePath(extension);
+ SetTempFilePath("ts");
var taskCompletionSource = new TaskCompletionSource<bool>();
@@ -97,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// OpenedMediaSource.Path = tempFile;
// OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
// OpenedMediaSource.Path = TempFilePath;
@@ -117,49 +103,46 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
- Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
+ Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
- public string GetFilePath()
- {
- return TempFilePath;
- }
-
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
- return Task.Run(async () =>
- {
- try
- {
- Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
- using var message = response;
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
- await StreamHelper.CopyToAsync(
- stream,
- fileStream,
- IODefaults.CopyToBufferSize,
- () => Resolve(openTaskCompletionSource),
- cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException ex)
+ return Task.Run(
+ async () =>
{
- Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
- openTaskCompletionSource.TrySetException(ex);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
- openTaskCompletionSource.TrySetException(ex);
- }
-
- openTaskCompletionSource.TrySetResult(false);
-
- EnableStreamSharing = false;
- await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
- }, CancellationToken.None);
+ try
+ {
+ Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
+ using var message = response;
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ IODefaults.CopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
+ }
+
+ openTaskCompletionSource.TrySetResult(false);
+
+ EnableStreamSharing = false;
+ await DeleteTempFiles(TempFilePath).ConfigureAwait(false);
+ },
+ CancellationToken.None);
}
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 4f21c66bc..18f17dda9 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -2,24 +2,24 @@
"Artists": "Kunstenare",
"Channels": "Kanale",
"Folders": "Lêergidse",
- "Favorites": "Gunstellinge",
+ "Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
- "HeaderAlbumArtists": "Album Kunstenaars",
+ "HeaderAlbumArtists": "Kunstenaars se Album",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Flieks",
"Shows": "Televisie Reekse",
"HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
- "Photos": "Fotos",
+ "Photos": "Foto's",
"Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
- "DeviceOnlineWithName": "{0} gekoppel is",
+ "DeviceOnlineWithName": "{0} is gekoppel",
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
@@ -71,7 +71,7 @@
"NameSeasonUnknown": "Seisoen Onbekend",
"NameSeasonNumber": "Seisoen {0}",
"NameInstallFailed": "{0} installering het misluk",
- "MusicVideos": "Musiek videos",
+ "MusicVideos": "Musiek Videos",
"Music": "Musiek",
"MixedContent": "Gemengde inhoud",
"MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
@@ -79,15 +79,15 @@
"MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
"MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
"Latest": "Nuutste",
- "LabelRunningTimeValue": "Lopende tyd: {0}",
+ "LabelRunningTimeValue": "Werktyd: {0}",
"LabelIpAddressValue": "IP adres: {0}",
"ItemRemovedWithName": "{0} is uit versameling verwyder",
- "ItemAddedWithName": "{0} is in die versameling",
- "HomeVideos": "Tuis opnames",
+ "ItemAddedWithName": "{0} is by die versameling gevoeg",
+ "HomeVideos": "Tuis Videos",
"HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
- "ChapterNameValue": "Hoofstuk",
+ "ChapterNameValue": "Hoofstuk {0}",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums",
@@ -117,5 +117,7 @@
"Forced": "Geforseer",
"Default": "Oorspronklik",
"TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
- "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
+ "TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon",
+ "TaskOptimizeDatabaseDescription": "Komprimeer databasis en verkort vrye ruimte. As hierdie taak uitgevoer word nadat die media versameling geskandeer is of ander veranderings aangebring is wat databasisaanpassings impliseer, kan dit die prestasie verbeter.",
+ "TaskOptimizeDatabase": "Optimaliseer databasis"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 3d6e159b1..9d4d40e51 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,5 +1,5 @@
{
- "Albums": "البومات",
+ "Albums": "ألبومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "الفنانين",
@@ -8,15 +8,15 @@
"CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}",
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
- "Collections": "مجموعات",
+ "Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
- "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
- "Favorites": "المفضلة",
+ "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
+ "Favorites": "مفضلات",
"Folders": "المجلدات",
"Genres": "التضنيفات",
- "HeaderAlbumArtists": "فناني الألبومات",
- "HeaderContinueWatching": "استئناف",
+ "HeaderAlbumArtists": "فناني الألبوم",
+ "HeaderContinueWatching": "استمر بالمشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "التلفاز المباشر",
"HeaderNextUp": "التالي",
"HeaderRecordingGroups": "مجموعات التسجيل",
- "HomeVideos": "الفيديوهات المنزلية",
+ "HomeVideos": "الفيديوهات الشخصية",
"Inherit": "توريث",
"ItemAddedWithName": "تم إضافة {0} للمكتبة",
"ItemRemovedWithName": "تم إزالة {0} من المكتبة",
@@ -33,7 +33,7 @@
"LabelRunningTimeValue": "المدة: {0}",
"Latest": "الأحدث",
"MessageApplicationUpdated": "لقد تم تحديث خادم Jellyfin",
- "MessageApplicationUpdatedTo": "تم تحديث سيرفر Jellyfin الى {0}",
+ "MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin الى {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}",
"MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
"MixedContent": "محتوى مختلط",
@@ -43,7 +43,7 @@
"NameInstallFailed": "فشل التثبيت {0}",
"NameSeasonNumber": "الموسم {0}",
"NameSeasonUnknown": "الموسم غير معروف",
- "NewVersionIsAvailable": "نسخة جديدة من سيرفر Jellyfin متوفرة للتحميل.",
+ "NewVersionIsAvailable": "نسخة جديدة من خادم Jellyfin متوفرة للتحميل.",
"NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق",
"NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق",
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
@@ -55,7 +55,7 @@
"NotificationOptionPluginInstalled": "تم تثبيت الملحق",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
- "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل السيرفر",
+ "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
"NotificationOptionTaskFailed": "فشل في المهمة المجدولة",
"NotificationOptionUserLockedOut": "تم إقفال حساب المستخدم",
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "يحتاج لإعادة تشغيله {0}",
"Shows": "الحلقات",
"Songs": "الأغاني",
- "StartupEmbyServerIsLoading": "سيرفر Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.",
+ "StartupEmbyServerIsLoading": "خادم Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.",
"SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}",
"SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} الى {1}",
"Sync": "مزامنة",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "حذف سجل الأنشطة",
"Default": "الإعدادات الافتراضية",
"Undefined": "غير معرف",
- "Forced": "ملحقة"
+ "Forced": "ملحقة",
+ "TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تشير ضمنًا إلى أن تعديلات قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
+ "TaskOptimizeDatabase": "تحسين قاعدة البيانات"
}
diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/as.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
new file mode 100644
index 000000000..56c4e7d39
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -0,0 +1,4 @@
+{
+ "Sync": "Сінхранізацыя",
+ "Playlists": "Плэйліст"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index bc25531d3..e1c923308 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
"Channels": "Канали",
"ChapterNameValue": "Глава {0}",
- "Collections": "Поредици",
+ "Collections": "Колекции",
"DeviceOfflineWithName": "{0} се разкачи",
"DeviceOnlineWithName": "{0} е свързан",
"FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
@@ -25,21 +25,21 @@
"HeaderLiveTV": "Телевизия на живо",
"HeaderNextUp": "Следва",
"HeaderRecordingGroups": "Запис групи",
- "HomeVideos": "Домашни клипове",
+ "HomeVideos": "Домашни Клипове",
"Inherit": "Наследяване",
"ItemAddedWithName": "{0} е добавено към библиотеката",
"ItemRemovedWithName": "{0} е премахнато от библиотеката",
- "LabelIpAddressValue": "ИП адрес: {0}",
- "LabelRunningTimeValue": "Стартирано от: {0}",
+ "LabelIpAddressValue": "IP адрес: {0}",
+ "LabelRunningTimeValue": "Продължителност: {0}",
"Latest": "Последни",
- "MessageApplicationUpdated": "Сървърът е обновен",
- "MessageApplicationUpdatedTo": "Сървърът е обновен до {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация се актуализира",
- "MessageServerConfigurationUpdated": "Конфигурацията на сървъра се актуализира",
+ "MessageApplicationUpdated": "Сървърът беше обновен",
+ "MessageApplicationUpdatedTo": "Сървърът беше обновен до {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация беше актуализирана",
+ "MessageServerConfigurationUpdated": "Конфигурацията на сървъра беше актуализирана",
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
- "MusicVideos": "Музикални видеа",
+ "MusicVideos": "Музикални Видеа",
"NameInstallFailed": "{0} не можа да се инсталира",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Неразпознат сезон",
@@ -118,5 +118,7 @@
"Forced": "Принудително",
"Default": "По подразбиране",
"TaskCleanActivityLogDescription": "Изтрива записите в дневника с активност по стари от конфигурираната възраст.",
- "TaskCleanActivityLog": "Изчисти дневника с активност"
+ "TaskCleanActivityLog": "Изчисти дневника с активност",
+ "TaskOptimizeDatabaseDescription": "Прави базата данни по-компактна и освобождава място. Пускането на тази задача след сканиране на библиотеката или правене на други промени, свързани с модификации на базата данни, може да подобри производителността.",
+ "TaskOptimizeDatabase": "Оптимизирай базата данни"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index fd8437b6d..2dee5e327 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
- "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
+ "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
"Channels": "Canals",
"ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
@@ -15,7 +15,7 @@
"Favorites": "Preferits",
"Folders": "Carpetes",
"Genres": "Gèneres",
- "HeaderAlbumArtists": "Artistes del Àlbum",
+ "HeaderAlbumArtists": "Àlbum de l'artista",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Predilectes",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "TV en Directe",
"HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups d'Enregistrament",
- "HomeVideos": "Vídeos domèstics",
+ "HomeVideos": "Vídeos Domèstics",
"Inherit": "Hereta",
"ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
"ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
@@ -39,7 +39,7 @@
"MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules",
"Music": "Música",
- "MusicVideos": "Vídeos musicals",
+ "MusicVideos": "Vídeos Musicals",
"NameInstallFailed": "Instalació de {0} fallida",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconeguda",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Buidar Registre d'Activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
- "Default": "Defecto"
+ "Default": "Defecto",
+ "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
+ "TaskOptimizeDatabase": "Optimitzar la base de dades"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index ff14c1929..25f51db16 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Televize",
"HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek",
- "HomeVideos": "Domáci videa",
+ "HomeVideos": "Domácí videa",
"Inherit": "Zdědit",
"ItemAddedWithName": "{0} byl přidán do knihovny",
"ItemRemovedWithName": "{0} byl odstraněn z knihovny",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Smazat záznam aktivity",
"Undefined": "Nedefinované",
"Forced": "Vynucené",
- "Default": "Výchozí"
+ "Default": "Výchozí",
+ "TaskOptimizeDatabaseDescription": "Zmenší databázi a odstraní prázdné místo. Spuštění této úlohy po skenování knihovny či jiných změnách databáze může zlepšit výkon.",
+ "TaskOptimizeDatabase": "Optimalizovat databázi"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json
new file mode 100644
index 000000000..1192f5c88
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/cy.json
@@ -0,0 +1,58 @@
+{
+ "DeviceOnlineWithName": "Mae {0} wedi'i gysylltu",
+ "DeviceOfflineWithName": "Mae {0} wedi datgysylltu",
+ "Default": "Diofyn",
+ "Collections": "Casgliadau",
+ "ChapterNameValue": "Pennod {0}",
+ "Channels": "Sianeli",
+ "CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
+ "Books": "Llyfrau",
+ "AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
+ "Artists": "Artistiaid",
+ "AppDeviceValues": "Ap: {0}, Dyfais: {1}",
+ "Albums": "Albwmau",
+ "Genres": "Genres",
+ "Folders": "Ffolderi",
+ "Favorites": "Ffefrynnau",
+ "LabelRunningTimeValue": "Amser rhedeg: {0}",
+ "TaskOptimizeDatabase": "Cronfa ddata Optimeiddio",
+ "TaskRefreshChannels": "Adnewyddu Sianeli",
+ "TaskRefreshPeople": "Adnewyddu Pobl",
+ "TasksChannelsCategory": "Sianeli Internet",
+ "VersionNumber": "Fersiwn {0}",
+ "ScheduledTaskStartedWithName": "{0} wedi dechrau",
+ "ScheduledTaskFailedWithName": "{0} wedi methu",
+ "ProviderValue": "Darparwr: {0}",
+ "NotificationOptionInstallationFailed": "Fethu Gosod",
+ "NameSeasonUnknown": "Tymor Anhysbys",
+ "NameSeasonNumber": "Tymor {0}",
+ "MusicVideos": "Fideos Cerddoriaeth",
+ "MixedContent": "Cynnwys amrywiol",
+ "HomeVideos": "Fideos Cartref",
+ "HeaderNextUp": "Nesaf i Fyny",
+ "HeaderFavoriteArtists": "Ffefryn Artistiaid",
+ "HeaderFavoriteAlbums": "Ffefryn Albwmau",
+ "HeaderContinueWatching": "Parhewch i Weithio",
+ "TasksApplicationCategory": "Rhaglen",
+ "TasksLibraryCategory": "Llyfrgell",
+ "TasksMaintenanceCategory": "Cynnal a Chadw",
+ "System": "System",
+ "Plugin": "Ategyn",
+ "Music": "Cerddoriaeth",
+ "Latest": "Diweddaraf",
+ "Inherit": "Etifeddu",
+ "Forced": "Orfodi",
+ "Application": "Rhaglen",
+ "HeaderAlbumArtists": "Artistiaid albwm",
+ "Sync": "Cysoni",
+ "Songs": "Caneuon",
+ "Shows": "Rhaglenni",
+ "Playlists": "Rhestri Chwarae",
+ "Photos": "Lluniau",
+ "ValueSpecialEpisodeName": "Arbennig - {0}",
+ "Movies": "Ffilmiau",
+ "Undefined": "Heb ddiffiniad",
+ "TvShows": "Rhaglenni teledu",
+ "HeaderLiveTV": "Teledu Byw",
+ "User": "Defnyddiwr"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 3453507d9..cfe365f57 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -15,7 +15,7 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albumkunstnere",
+ "HeaderAlbumArtists": "Kunstnerens album",
"HeaderContinueWatching": "Fortsæt Afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Ryd Aktivitetslog",
"Undefined": "Udefineret",
"Forced": "Tvunget",
- "Default": "Standard"
+ "Default": "Standard",
+ "TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.",
+ "TaskOptimizeDatabase": "Optimér database"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 9d82b5878..c924e5c15 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
- "AuthenticationSucceededWithUserName": "{0} wurde angemeldet",
+ "AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle",
@@ -16,7 +16,7 @@
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
- "HeaderContinueWatching": "Fortsetzen",
+ "HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
"Undefined": "Undefiniert",
"Forced": "Erzwungen",
- "Default": "Standard"
+ "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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 23d45b473..697063f26 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -1,5 +1,5 @@
{
- "Albums": "Άλμπουμς",
+ "Albums": "Άλμπουμ",
"AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
"Application": "Εφαρμογή",
"Artists": "Καλλιτέχνες",
@@ -15,7 +15,7 @@
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
- "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
+ "HeaderAlbumArtists": "Άλμπουμ Καλλιτέχνη",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@@ -39,7 +39,7 @@
"MixedContent": "Ανάμεικτο Περιεχόμενο",
"Movies": "Ταινίες",
"Music": "Μουσική",
- "MusicVideos": "Μουσικά βίντεο",
+ "MusicVideos": "Μουσικά Βίντεο",
"NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
"NameSeasonNumber": "Κύκλος {0}",
"NameSeasonUnknown": "Άγνωστος Κύκλος",
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlaybackStopped": "Η αναπαραγωγή βίντεο σταμάτησε",
"Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής",
- "Plugin": "Plugin",
+ "Plugin": "Πρόσθετο",
"PluginInstalledWithName": "{0} εγκαταστήθηκε",
"PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
"PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
"Undefined": "Απροσδιόριστο",
"Forced": "Εξαναγκασμένο",
- "Default": "Προεπιλογή"
+ "Default": "Προεπιλογή",
+ "TaskOptimizeDatabaseDescription": "Συμπιέζει τη βάση δεδομένων και δημιουργεί ελεύθερο χώρο. Η εκτέλεση αυτής της εργασίας μετά τη σάρωση της βιβλιοθήκης ή την πραγματοποίηση άλλων αλλαγών που συνεπάγονται τροποποιήσεις της βάσης δεδομένων μπορεί να βελτιώσει την απόδοση.",
+ "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 7667612b9..add578376 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -15,7 +15,7 @@
"Favorites": "Favourites",
"Folders": "Folders",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album Artists",
+ "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
- "HomeVideos": "Home videos",
+ "HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
@@ -39,7 +39,7 @@
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
- "MusicVideos": "Music videos",
+ "MusicVideos": "Music Videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Clean Activity Log",
"Undefined": "Undefined",
"Forced": "Forced",
- "Default": "Default"
+ "Default": "Default",
+ "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
+ "TaskOptimizeDatabase": "Optimise database"
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 65964f6d9..568a8e447 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -12,12 +12,12 @@
"Default": "Default",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
- "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
+ "FailedLoginAttemptWithUserName": "Failed login try from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Forced": "Forced",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album Artists",
+ "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
@@ -27,7 +27,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
- "HomeVideos": "Home videos",
+ "HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
@@ -41,7 +41,7 @@
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
- "MusicVideos": "Music videos",
+ "MusicVideos": "Music Videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index ca615cc8c..8abf7fa66 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -1,17 +1,17 @@
{
- "NotificationOptionInstallationFailed": "Instalada fiasko",
- "NotificationOptionAudioPlaybackStopped": "Sono de ludado haltis",
- "NotificationOptionAudioPlayback": "Ludado de sono startis",
+ "NotificationOptionInstallationFailed": "Instalada malsukceso",
+ "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis",
+ "NotificationOptionAudioPlayback": "Ludado de sono lanĉis",
"NameSeasonUnknown": "Sezono Nekonata",
"NameSeasonNumber": "Sezono {0}",
"NameInstallFailed": "{0} instalado fiaskis",
"Music": "Muziko",
"Movies": "Filmoj",
- "ItemRemovedWithName": "{0} forigis el la biblioteko",
- "ItemAddedWithName": "{0} aldonis al la biblioteko",
- "HeaderLiveTV": "Viva Televido",
- "HeaderContinueWatching": "Daŭrigi Spektado",
- "HeaderAlbumArtists": "Artistoj de Albumo",
+ "ItemRemovedWithName": "{0} forigis el la plurmediteko",
+ "ItemAddedWithName": "{0} aldonis al la plurmediteko",
+ "HeaderLiveTV": "TV-etero",
+ "HeaderContinueWatching": "Daŭrigi Spektadon",
+ "HeaderAlbumArtists": "Artistoj de albumo",
"Folders": "Dosierujoj",
"DeviceOnlineWithName": "{0} estas konektita",
"Default": "Defaŭlte",
@@ -23,14 +23,14 @@
"Application": "Aplikaĵo",
"AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
"Albums": "Albumoj",
- "TasksLibraryCategory": "Libraro",
+ "TasksLibraryCategory": "Plurmediteko",
"VersionNumber": "Versio {0}",
"UserDownloadingItemWithValues": "{0} elŝutas {1}",
"UserCreatedWithName": "Uzanto {0} kreiĝis",
"User": "Uzanto",
"System": "Sistemo",
"Songs": "Kantoj",
- "ScheduledTaskStartedWithName": "{0} komencis",
+ "ScheduledTaskStartedWithName": "{0} lanĉis",
"ScheduledTaskFailedWithName": "{0} malsukcesis",
"PluginUninstalledWithName": "{0} malinstaliĝis",
"PluginInstalledWithName": "{0} instaliĝis",
@@ -43,5 +43,81 @@
"MusicVideos": "Muzikvideoj",
"LabelIpAddressValue": "IP-adreso: {0}",
"Genres": "Ĝenroj",
- "DeviceOfflineWithName": "{0} malkonektis"
+ "DeviceOfflineWithName": "{0} malkonektis",
+ "HeaderFavoriteArtists": "Favorataj Artistoj",
+ "Shows": "Serioj",
+ "HeaderFavoriteShows": "Favorataj Serioj",
+ "TvShows": "TV-serioj",
+ "Favorites": "Favorataj",
+ "TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
+ "TaskRefreshLibrary": "Skani Plurmeditekon",
+ "ValueSpecialEpisodeName": "Speciala - {0}",
+ "TaskOptimizeDatabase": "Optimumigi datenbazon",
+ "TaskRefreshChannels": "Refreŝigi Kanalojn",
+ "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
+ "TaskRefreshPeople": "Refreŝigi Homojn",
+ "TasksChannelsCategory": "Interretaj Kanaloj",
+ "ProviderValue": "Provizanto: {0}",
+ "NotificationOptionPluginError": "Kromprogramo malsukcesis",
+ "MixedContent": "Miksita enhavo",
+ "TasksApplicationCategory": "Aplikaĵo",
+ "TasksMaintenanceCategory": "Prizorgado",
+ "Undefined": "Nedifinita",
+ "Sync": "Sinkronigo",
+ "Latest": "Plej novaj",
+ "Inherit": "Hereda",
+ "HomeVideos": "Hejmaj Videoj",
+ "HeaderNextUp": "Sekva Plue",
+ "HeaderFavoriteSongs": "Favorataj Kantoj",
+ "HeaderFavoriteEpisodes": "Favorataj Epizodoj",
+ "HeaderFavoriteAlbums": "Favorataj Albumoj",
+ "Forced": "Forcita",
+ "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
+ "NotificationOptionVideoPlayback": "La videoludado lanĉis",
+ "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
+ "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
+ "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.",
+ "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.",
+ "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.",
+ "TaskCleanLogsDescription": "Forigas ĵurnalajn dosierojn aĝajn pli ol {0} tagojn.",
+ "TaskRefreshLibraryDescription": "Skanas vian plurmeditekon por novaj dosieroj kaj refreŝigas metadatenaron.",
+ "NewVersionIsAvailable": "Nova versio de Jellyfin Server estas elŝutebla.",
+ "TaskCleanCacheDescription": "Forigas stapla dosierojn ne plu necesajn de la sistemo.",
+ "TaskCleanActivityLogDescription": "Forigas aktivecan ĵurnalaĵojn pli malnovajn ol la agordita aĝo.",
+ "TaskCleanTranscodeDescription": "Forigas transkodajn dosierojn aĝajn pli ol unu tagon.",
+ "ValueHasBeenAddedToLibrary": "{0} estis aldonita al via plurmediteko",
+ "SubtitleDownloadFailureFromForItem": "Subtekstoj malsukcesis elŝuti de {0} por {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin Server ŝarĝas. Provi denove baldaŭ.",
+ "TaskRefreshChapterImagesDescription": "Kreas bildetojn por videoj kiuj havas ĉapitrojn.",
+ "UserStoppedPlayingItemWithValues": "{0} finis ludi {1} ĉe {2}",
+ "UserPolicyUpdatedWithName": "Uzanta politiko estis ĝisdatigita por {0}",
+ "UserPasswordChangedWithName": "Pasvorto estis ŝanĝita por uzanto {0}",
+ "UserStartedPlayingItemWithValues": "{0} ludas {1} ĉe {2}",
+ "UserLockedOutWithName": "Uzanto {0} estas elŝlosita",
+ "UserOnlineFromDevice": "{0} estas enreta de {1}",
+ "UserOfflineFromDevice": "{0} malkonektis de {1}",
+ "UserDeletedWithName": "Uzanto {0} estis forigita",
+ "MessageServerConfigurationUpdated": "Servila agordaro estis ĝisdatigita",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Servila agorda sekcio {0} estis ĝisdatigita",
+ "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
+ "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
+ "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
+ "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
+ "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
+ "TaskRefreshChapterImages": "Eltiri Ĉapitrajn Bildojn",
+ "TaskCleanCache": "Malplenigi Staplan Katalogon",
+ "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
+ "PluginUpdatedWithName": "{0} estis ĝisdatigita",
+ "NotificationOptionVideoPlaybackStopped": "La videoludado haltis",
+ "NotificationOptionUserLockedOut": "Uzanto ŝlosita",
+ "NotificationOptionTaskFailed": "Planita tasko malsukcesis",
+ "NotificationOptionPluginUpdateInstalled": "Ĝisdatigo de kromprogramo instalita",
+ "NotificationOptionCameraImageUploaded": "Kamera bildo alŝutita",
+ "NotificationOptionApplicationUpdateInstalled": "Aplikaĵa ĝisdatigo instalita",
+ "NotificationOptionApplicationUpdateAvailable": "Ĝisdatigo de aplikaĵo havebla",
+ "LabelRunningTimeValue": "Ludada tempo: {0}",
+ "HeaderRecordingGroups": "Rikordadaj Grupoj",
+ "FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}",
+ "CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}",
+ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 0d4a14be0..6321f695c 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Borrar log de actividades",
"Undefined": "Indefinido",
"Forced": "Forzado",
- "Default": "Por Defecto"
+ "Default": "Por Defecto",
+ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
+ "TaskOptimizeDatabase": "Optimización de base de datos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 5d7ed243f..432814dac 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -15,7 +15,7 @@
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas del álbum",
+ "HeaderAlbumArtists": "Artistas del Álbum",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A continuación",
"HeaderRecordingGroups": "Grupos de grabación",
- "HomeVideos": "Videos caseros",
+ "HomeVideos": "Videos Caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
@@ -39,7 +39,7 @@
"MixedContent": "Contenido mezclado",
"Movies": "Películas",
"Music": "Música",
- "MusicVideos": "Videos musicales",
+ "MusicVideos": "Videos Musicales",
"NameInstallFailed": "Falló la instalación de {0}",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconocida",
@@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
- "NotificationOptionInstallationFailed": "Falla de instalación",
+ "NotificationOptionInstallationFailed": "Fallo en la instalación",
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
"NotificationOptionPluginError": "Falla de complemento",
"NotificationOptionPluginInstalled": "Complemento instalado",
@@ -69,7 +69,7 @@
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado",
- "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
+ "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Programas",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
@@ -94,9 +94,9 @@
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
- "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
+ "TaskRefreshChannelsDescription": "Actualiza la información de los canales de Internet.",
"TaskRefreshChannels": "Actualizar canales",
- "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+ "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día de antigüedad.",
"TaskCleanTranscode": "Limpiar directorio de transcodificado",
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
"Forced": "Forzado",
- "Default": "Predeterminado"
+ "Default": "Predeterminado",
+ "TaskOptimizeDatabase": "Optimizar base de datos",
+ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 91939843f..f8c69712e 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -16,7 +16,7 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderContinueWatching": "Continuar viendo",
+ "HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
- "Shows": "Series de Televisión",
+ "Shows": "Series",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index 6d2a5c7ac..2ca736ad9 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -15,7 +15,7 @@
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderContinueWatching": "Continuar viendo",
- "HeaderAlbumArtists": "Artistas del álbum",
+ "HeaderAlbumArtists": "Artistas de álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
@@ -29,7 +29,7 @@
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
"TaskRefreshChannels": "Actualizar canales",
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
- "TaskCleanTranscode": "Limpiar directorio de transcodificado",
+ "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos",
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
@@ -105,7 +105,7 @@
"Inherit": "Heredar",
"HomeVideos": "Videos caseros",
"HeaderRecordingGroups": "Grupos de grabación",
- "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
+ "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
"DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado",
"ChapterNameValue": "Capítulo {0}",
@@ -114,8 +114,10 @@
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
- "TaskCleanActivityLog": "Limpiar Registro de Actividades",
+ "TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
"Forced": "Forzado",
- "Default": "Por Defecto"
+ "Default": "Por defecto",
+ "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
+ "TaskOptimizeDatabase": "Optimizar base de datos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
new file mode 100644
index 000000000..8db6a0b38
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -0,0 +1,123 @@
+{
+ "TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
+ "UserDownloadingItemWithValues": "{0} laeb alla {1}",
+ "HeaderRecordingGroups": "Salvestusrühmad",
+ "TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
+ "TaskOptimizeDatabase": "Optimeeri andmebaasi",
+ "TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
+ "TaskDownloadMissingSubtitles": "Laadi alla puuduvad subtiitrid",
+ "TaskRefreshChannelsDescription": "Värskendab veebikanalite teavet.",
+ "TaskRefreshChannels": "Värskenda kanaleid",
+ "TaskCleanTranscodeDescription": "Kustutab üle ühe päeva vanused transkodeerimisfailid.",
+ "TaskCleanTranscode": "Puhasta transkoodimise kataloog",
+ "TaskUpdatePluginsDescription": "Laadib alla ja paigaldab nende pluginate uuendused, mis on seadistatud automaatselt uuenduma.",
+ "TaskUpdatePlugins": "Uuenda pluginaid",
+ "TaskRefreshPeopleDescription": "Värskendab meediakogus näitlejate ja režissööride metaandmeid.",
+ "TaskRefreshPeople": "Värskenda inimesi",
+ "TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.",
+ "TaskCleanLogs": "Puhasta logikataloog",
+ "TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.",
+ "Collections": "Kogumikud",
+ "TaskRefreshLibrary": "Skaneeri meediakogu",
+ "TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.",
+ "TaskRefreshChapterImages": "Eralda peatükipildid",
+ "TaskCleanCacheDescription": "Kustutab vahemälufailid, mida süsteem enam ei vaja.",
+ "TaskCleanCache": "Puhasta vahemälu kataloog",
+ "TaskCleanActivityLog": "Puhasta tegevuslogi",
+ "TasksChannelsCategory": "Veebikanalid",
+ "TasksApplicationCategory": "Rakendus",
+ "TasksLibraryCategory": "Meediakogu",
+ "TasksMaintenanceCategory": "Hooldus",
+ "VersionNumber": "Versioon {0}",
+ "ValueSpecialEpisodeName": "Eriepisood - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
+ "UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
+ "UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
+ "UserLockedOutWithName": "Kasutaja {0} lukustati",
+ "UserDeletedWithName": "Kasutaja {0} kustutati",
+ "UserCreatedWithName": "Kasutaja {0} on loodud",
+ "ScheduledTaskStartedWithName": "{0} käivitati",
+ "ProviderValue": "Allikas: {0}",
+ "StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
+ "User": "Kasutaja",
+ "Undefined": "Määratlemata",
+ "TvShows": "Seriaalid",
+ "System": "Süsteem",
+ "Sync": "Sünkrooni",
+ "Songs": "Laulud",
+ "Shows": "Sarjad",
+ "ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
+ "ScheduledTaskFailedWithName": "{0} nurjus",
+ "PluginUpdatedWithName": "{0} uuendati",
+ "PluginUninstalledWithName": "{0} eemaldati",
+ "PluginInstalledWithName": "{0} paigaldati",
+ "Plugin": "Plugin",
+ "Playlists": "Pleilistid",
+ "Photos": "Fotod",
+ "NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
+ "NotificationOptionVideoPlayback": "Video taasesitus algas",
+ "NotificationOptionUserLockedOut": "Kasutaja lukustati",
+ "NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus",
+ "NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine",
+ "NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus",
+ "NotificationOptionPluginUninstalled": "Plugin eemaldati",
+ "NotificationOptionPluginInstalled": "Plugin paigaldati",
+ "NotificationOptionPluginError": "Plugina tõrge",
+ "NotificationOptionNewLibraryContent": "Lisati uut sisu",
+ "NotificationOptionInstallationFailed": "Paigaldamine nurjus",
+ "NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud",
+ "NotificationOptionAudioPlaybackStopped": "Heli taasesitus lõppes",
+ "NotificationOptionAudioPlayback": "Heli taasesitus algas",
+ "NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati",
+ "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
+ "NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
+ "NameSeasonUnknown": "Tundmatu hooaeg",
+ "NameSeasonNumber": "Hooaeg {0}",
+ "NameInstallFailed": "{0} paigaldamine nurjus",
+ "MusicVideos": "Muusikavideod",
+ "Music": "Muusika",
+ "Movies": "Filmid",
+ "MixedContent": "Segatud sisu",
+ "MessageServerConfigurationUpdated": "Serveri seadistust uuendati",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serveri seadistusosa {0} uuendati",
+ "MessageApplicationUpdatedTo": "Jellyfin server uuendati versioonile {0}",
+ "MessageApplicationUpdated": "Jellyfin server uuendati",
+ "Latest": "Uusimad",
+ "LabelRunningTimeValue": "Kestus: {0}",
+ "LabelIpAddressValue": "IP aadress: {0}",
+ "ItemRemovedWithName": "{0} eemaldati meediakogust",
+ "ItemAddedWithName": "{0} lisati meediakogusse",
+ "Inherit": "Päri",
+ "HomeVideos": "Koduvideod",
+ "HeaderNextUp": "Järgmisena",
+ "HeaderLiveTV": "Otse TV",
+ "HeaderFavoriteSongs": "Lemmiklood",
+ "HeaderFavoriteShows": "Lemmikseriaalid",
+ "HeaderFavoriteEpisodes": "Lemmikepisoodid",
+ "HeaderFavoriteArtists": "Lemmikesitajad",
+ "HeaderFavoriteAlbums": "Lemmikalbumid",
+ "HeaderContinueWatching": "Jätka vaatamist",
+ "HeaderAlbumArtists": "Albumi esitajad",
+ "Genres": "Žanrid",
+ "Forced": "Sunnitud",
+ "Folders": "Kaustad",
+ "Favorites": "Lemmikud",
+ "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus",
+ "DeviceOnlineWithName": "{0} on ühendatud",
+ "DeviceOfflineWithName": "{0} katkestas ühenduse",
+ "Default": "Vaikimisi",
+ "ChapterNameValue": "Peatükk {0}",
+ "Channels": "Kanalid",
+ "CameraImageUploadedFrom": "Uus kaamera pilt laaditi üles allikalt {0}",
+ "Books": "Raamatud",
+ "AuthenticationSucceededWithUserName": "{0} autentimine õnnestus",
+ "Artists": "Esitajad",
+ "Application": "Rakendus",
+ "AppDeviceValues": "Rakendus: {0}, seade: {1}",
+ "Albums": "Albumid",
+ "UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}",
+ "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
+ "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
+ "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
+ "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 8ab657e5b..6960ff007 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -6,7 +6,7 @@
"AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
"Books": "کتاب‌ها",
"CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
- "Channels": "کانال‌ها",
+ "Channels": "کانالها",
"ChapterNameValue": "قسمت {0}",
"Collections": "مجموعه‌ها",
"DeviceOfflineWithName": "ارتباط {0} قطع شد",
@@ -37,7 +37,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
"MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
"MixedContent": "محتوای مخلوط",
- "Movies": "فیلم‌ها",
+ "Movies": "فیلم ها",
"Music": "موسیقی",
"MusicVideos": "موزیک ویدیوها",
"NameInstallFailed": "{0} نصب با مشکل مواجه شد",
@@ -118,5 +118,6 @@
"Default": "پیشفرض",
"TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.",
"TaskCleanActivityLog": "پاکسازی سیاهه فعالیت",
- "Undefined": "تعریف نشده"
+ "Undefined": "تعریف نشده",
+ "TaskOptimizeDatabase": "بهینه سازی پایگاه داده"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 633968d26..4a1f4f1d5 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -117,5 +117,7 @@
"Default": "Oletus",
"TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
"TaskCleanActivityLog": "Tyhjennä toimintahistoria",
- "Undefined": "Määrittelemätön"
+ "Undefined": "Määrittelemätön",
+ "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.",
+ "TaskOptimizeDatabase": "Optimoi tietokanta"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index f18a1c030..99839ae6e 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -117,5 +117,7 @@
"TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas luma sa nakatakda na edad.",
"Default": "Default",
"Undefined": "Hindi tiyak",
- "Forced": "Sapilitan"
+ "Forced": "Sapilitan",
+ "TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.",
+ "TaskOptimizeDatabase": "I-optimize ang database"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3c51d64e0..2a56d0745 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Éfface les entrées du journal plus anciennes que l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Indéfini",
- "Forced": "Forcé"
+ "Forced": "Forcé",
+ "TaskOptimizeDatabaseDescription": "Compacte la base de données et tronque l'espace libre. Lancer cette tâche après avoir scanné la bibliothèque ou faire d'autres changements impliquant des modifications de la base peuvent ameliorer les performances.",
+ "TaskOptimizeDatabase": "Optimiser la base de données"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index ce1493be8..d60955d5f 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -1,7 +1,7 @@
{
"Albums": "Albums",
"AppDeviceValues": "Application : {0}, Appareil : {1}",
- "Application": "Application",
+ "Application": "Applications",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
@@ -15,7 +15,7 @@
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
+ "HeaderAlbumArtists": "Artistes d'album",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Rafraîchir les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
- "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
+ "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshLibrary": "Scanner la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Non défini",
"Forced": "Forcé",
- "Default": "Par défaut"
+ "Default": "Par défaut",
+ "TaskOptimizeDatabaseDescription": "Réduit les espaces vides/inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
+ "TaskOptimizeDatabase": "Optimiser la base de données"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 0398e1c9e..b433c6f68 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -48,7 +48,7 @@
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteAlbums": "Álbunes Favoritos",
"HeaderContinueWatching": "Seguir mirando",
- "HeaderAlbumArtists": "Artistas de Album",
+ "HeaderAlbumArtists": "Artistas do Album",
"Genres": "Xéneros",
"Forced": "Forzado",
"Folders": "Cartafoles",
@@ -88,5 +88,36 @@
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
- "NotificationOptionTaskFailed": "Falla na tarefa axendada"
+ "NotificationOptionTaskFailed": "Falla na tarefa axendada",
+ "TaskCleanTranscodeDescription": "Borra os arquivos de transcode anteriores a un día.",
+ "TaskCleanTranscode": "Limpar Directorio de Transcode",
+ "UserStoppedPlayingItemWithValues": "{0} rematou de reproducir {1} en {2}",
+ "UserStartedPlayingItemWithValues": "{0} está reproducindo {1} en {2}",
+ "TaskDownloadMissingSubtitlesDescription": "Busca en internet por subtítulos que faltan baseado na configuración de metadatos.",
+ "TaskDownloadMissingSubtitles": "Descargar subtítulos que faltan",
+ "TaskRefreshChannelsDescription": "Refresca a información do canle de internet.",
+ "TaskRefreshChannels": "Refrescar Canles",
+ "TaskUpdatePluginsDescription": "Descarga e instala actualizacións para plugins que están configurados para actualizarse automáticamente.",
+ "TaskRefreshPeopleDescription": "Actualiza os metadatos dos actores e directores na túa libraría multimedia.",
+ "TaskRefreshPeople": "Refrescar Persoas",
+ "TaskCleanLogsDescription": "Borra arquivos de rexistro que son mais antigos que {0} días.",
+ "TaskRefreshLibraryDescription": "Escanea a tua libraría multimedia buscando novos arquivos e refrescando os metadatos.",
+ "TaskRefreshLibrary": "Escanear Libraría Multimedia",
+ "TaskRefreshChapterImagesDescription": "Crea previsualizacións para videos que teñen capítulos.",
+ "TaskRefreshChapterImages": "Extraer Imaxes dos Capítulos",
+ "TaskCleanCacheDescription": "Borra ficheiros da caché que xa non son necesarios para o sistema.",
+ "TaskCleanCache": "Limpa Directorio de Caché",
+ "TaskCleanActivityLogDescription": "Borra as entradas no rexistro de actividade anteriores á data configurada.",
+ "TasksApplicationCategory": "Aplicación",
+ "ValueSpecialEpisodeName": "Especial - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} foi engadido a túa libraría multimedia",
+ "TasksLibraryCategory": "Libraría",
+ "TasksMaintenanceCategory": "Mantemento",
+ "VersionNumber": "Versión {0}",
+ "UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
+ "UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
+ "UserOnlineFromDevice": "{0} está en liña desde {1}",
+ "UserOfflineFromDevice": "{0} desconectouse desde {1}",
+ "TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
+ "TaskOptimizeDatabase": "Optimizar base de datos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 9eb80b83b..4df0444e6 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -15,7 +15,7 @@
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
- "HeaderAlbumArtists": "Izvođači na albumu",
+ "HeaderAlbumArtists": "Izvođači albuma",
"HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 85848fed6..acde84aaf 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -11,11 +11,11 @@
"Collections": "Gyűjtemények",
"DeviceOfflineWithName": "{0} kijelentkezett",
"DeviceOnlineWithName": "{0} belépett",
- "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}",
+ "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Genres": "Műfajok",
- "HeaderAlbumArtists": "Album előadók",
+ "HeaderAlbumArtists": "Album előadó(k)",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Tevékenységnapló törlése",
"Undefined": "Meghatározatlan",
"Forced": "Kényszerített",
- "Default": "Alapértelmezett"
+ "Default": "Alapértelmezett",
+ "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.",
+ "TaskOptimizeDatabase": "Adatbázis optimalizálása"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index ba3513870..37d59abd9 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -7,10 +7,10 @@
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}",
- "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
+ "ItemRemovedWithName": "{0} sudah dihapus dari pustaka",
"ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
- "Inherit": "Warisan",
- "HomeVideos": "Video Rumah",
+ "Inherit": "Warisi",
+ "HomeVideos": "Video Rumahan",
"HeaderRecordingGroups": "Grup Rekaman",
"HeaderNextUp": "Selanjutnya",
"HeaderLiveTV": "TV Live",
@@ -73,7 +73,7 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
- "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
+ "NewVersionIsAvailable": "Versi baru dari Jellyfin Server sudah tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}",
"NameInstallFailed": "{0} penginstalan gagal",
@@ -117,5 +117,7 @@
"TaskCleanActivityLog": "Bersihkan Log Aktivitas",
"Undefined": "Tidak terdefinisi",
"Forced": "Dipaksa",
- "Default": "Bawaan"
+ "Default": "Bawaan",
+ "TaskOptimizeDatabaseDescription": "Rapihkan basis data dan membersihkan ruang kosong. Menjalankan tugas ini setelah memindai pustaka atau melakukan perubahan lain yang menyiratkan modifikasi basis data dapat meningkatkan kinerja.",
+ "TaskOptimizeDatabase": "Optimalkan basis data"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index bd06f0a25..4c4de4999 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -15,7 +15,7 @@
"Favorites": "Preferiti",
"Folders": "Cartelle",
"Genres": "Generi",
- "HeaderAlbumArtists": "Artisti degli Album",
+ "HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo",
"HeaderRecordingGroups": "Gruppi di Registrazione",
- "HomeVideos": "Video personali",
+ "HomeVideos": "Video Personali",
"Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
@@ -39,7 +39,7 @@
"MixedContent": "Contenuto misto",
"Movies": "Film",
"Music": "Musica",
- "MusicVideos": "Video musicali",
+ "MusicVideos": "Video Musicali",
"NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviati",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
- "Shows": "Programmi",
+ "Shows": "Serie TV",
"Songs": "Canzoni",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata.",
"Undefined": "Non Definito",
"Forced": "Forzato",
- "Default": "Predefinito"
+ "Default": "Predefinito",
+ "TaskOptimizeDatabaseDescription": "Compatta Database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altri cambiamenti inerenti il database potrebbe aumentarne la performance.",
+ "TaskOptimizeDatabase": "Ottimizza Database"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index fa0ab8b92..2588f1e8c 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -11,12 +11,12 @@
"Collections": "コレクション",
"DeviceOfflineWithName": "{0} が切断されました",
"DeviceOnlineWithName": "{0} が接続されました",
- "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0}によって失敗しました",
+ "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました",
"Favorites": "お気に入り",
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderContinueWatching": "視聴を続ける",
+ "HeaderContinueWatching": "続きを見る",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
@@ -117,5 +117,7 @@
"TaskCleanActivityLog": "アクティビティの履歴を消去",
"Undefined": "未定義",
"Forced": "強制",
- "Default": "デフォルト"
+ "Default": "デフォルト",
+ "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。",
+ "TaskOptimizeDatabase": "データベースの最適化"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 4eee36989..1b4a18deb 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -118,5 +118,7 @@
"TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.",
"TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
- "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady."
+ "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady.",
+ "TaskOptimizeDatabaseDescription": "Derekqordy qysyp, bos oryndy qysqartady. Būl tapsyrmany tasyğyşhanany skanerlegennen keiın nemese derekqorğa meñzeitın basqa özgertuler ıstelgennen keiın oryndau önımdılıktı damytuy mümkın.",
+ "TaskOptimizeDatabase": "Derekqordy oñtailandyru"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 9179bbc8d..a37de0748 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -15,7 +15,7 @@
"Favorites": "즐겨찾기",
"Folders": "폴더",
"Genres": "장르",
- "HeaderAlbumArtists": "앨범 아티스트",
+ "HeaderAlbumArtists": "아티스트의 앨범",
"HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "즐겨찾는 앨범",
"HeaderFavoriteArtists": "즐겨찾는 아티스트",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "활동내역청소",
"Undefined": "일치하지 않음",
"Forced": "강제하기",
- "Default": "기본 설정"
+ "Default": "기본 설정",
+ "TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.",
+ "TaskOptimizeDatabase": "데이터베이스 최적화"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index f3a131d40..f0a07f604 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -1,7 +1,7 @@
{
"Albums": "Albumai",
"AppDeviceValues": "Programa: {0}, Įrenginys: {1}",
- "Application": "Programa",
+ "Application": "Programėlė",
"Artists": "Atlikėjai",
"AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
"Books": "Knygos",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index a46bdc3de..443a74a10 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -117,5 +117,7 @@
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
"Undefined": "Nenoteikts",
- "Default": "Noklusējums"
+ "Default": "Noklusējums",
+ "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
+ "TaskOptimizeDatabase": "Optimizēt datubāzi"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index b780ef498..279734c5e 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -5,7 +5,7 @@
"PluginUninstalledWithName": "{0} беше успешно деинсталирано",
"PluginInstalledWithName": "{0} беше успешно инсталирано",
"Plugin": "Додатоци",
- "Playlists": "Листи",
+ "Playlists": "Плејлисти",
"Photos": "Слики",
"NotificationOptionVideoPlaybackStopped": "Видео стопирано",
"NotificationOptionVideoPlayback": "Видео пуштено",
@@ -50,7 +50,7 @@
"HeaderFavoriteEpisodes": "Омилени Епизоди",
"HeaderFavoriteArtists": "Омилени Изведувачи",
"HeaderFavoriteAlbums": "Омилени Албуми",
- "HeaderContinueWatching": "Продолжи со гледање",
+ "HeaderContinueWatching": "Продолжи со Гледање",
"HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови",
"Folders": "Папки",
@@ -97,5 +97,8 @@
"TasksChannelsCategory": "Интернет Канали",
"TasksApplicationCategory": "Апликација",
"TasksLibraryCategory": "Библиотека",
- "TasksMaintenanceCategory": "Одржување"
+ "TasksMaintenanceCategory": "Одржување",
+ "Undefined": "Недефинирано",
+ "Forced": "Принудно",
+ "Default": "Зададено"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index e764963cf..09ef34913 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -103,7 +103,7 @@
"ValueSpecialEpisodeName": "പ്രത്യേക - {0}",
"Collections": "ശേഖരങ്ങൾ",
"Folders": "ഫോൾഡറുകൾ",
- "HeaderAlbumArtists": "ആൽബം ആർട്ടിസ്റ്റുകൾ",
+ "HeaderAlbumArtists": "കലാകാരന്റെ ആൽബം",
"Sync": "സമന്വയിപ്പിക്കുക",
"Movies": "സിനിമകൾ",
"Photos": "ഫോട്ടോകൾ",
@@ -117,5 +117,7 @@
"Favorites": "പ്രിയങ്കരങ്ങൾ",
"Books": "പുസ്തകങ്ങൾ",
"Genres": "വിഭാഗങ്ങൾ",
- "Channels": "ചാനലുകൾ"
+ "Channels": "ചാനലുകൾ",
+ "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
+ "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json
new file mode 100644
index 000000000..7421d42fb
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/mn.json
@@ -0,0 +1,14 @@
+{
+ "Books": "Номууд",
+ "HeaderNextUp": "Дараах",
+ "HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
+ "Songs": "Дуунууд",
+ "Playlists": "Тоглуулах жагсаалт",
+ "Movies": "Кино",
+ "Latest": "Сүүлийн үеийн",
+ "Genres": "Төрөл зүйл",
+ "Favorites": "Дуртай",
+ "Collections": "Багц",
+ "Artists": "Зураачуд",
+ "Albums": "Цомгууд"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 5b4c8ae10..deb28970c 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -2,10 +2,10 @@
"Albums": "Album-album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi",
- "Artists": "Artis",
+ "Artists": "Artis-artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
- "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
+ "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
"Channels": "Saluran",
"ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
@@ -37,9 +37,9 @@
"MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
"MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
"MixedContent": "Kandungan campuran",
- "Movies": "Filem",
+ "Movies": "Filem-filem",
"Music": "Muzik",
- "MusicVideos": "Muzik video",
+ "MusicVideos": "Video muzik",
"NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Musim Tidak Diketahui",
@@ -53,43 +53,43 @@
"NotificationOptionNewLibraryContent": "Kandungan baru telah ditambah",
"NotificationOptionPluginError": "Kegagalan plugin",
"NotificationOptionPluginInstalled": "Plugin telah dipasang",
- "NotificationOptionPluginUninstalled": "Plugin uninstalled",
- "NotificationOptionPluginUpdateInstalled": "Plugin update installed",
- "NotificationOptionServerRestartRequired": "Server restart required",
- "NotificationOptionTaskFailed": "Scheduled task failure",
- "NotificationOptionUserLockedOut": "User locked out",
- "NotificationOptionVideoPlayback": "Video playback started",
+ "NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
+ "NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
+ "NotificationOptionServerRestartRequired": "",
+ "NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
+ "NotificationOptionUserLockedOut": "Pengguna telah dikunci",
+ "NotificationOptionVideoPlayback": "Ulangmain video bermula",
"NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
"Photos": "Gambar-gambar",
"Playlists": "Senarai main",
"Plugin": "Plugin",
- "PluginInstalledWithName": "{0} was installed",
- "PluginUninstalledWithName": "{0} was uninstalled",
- "PluginUpdatedWithName": "{0} was updated",
- "ProviderValue": "Provider: {0}",
+ "PluginInstalledWithName": "{0} telah dipasang",
+ "PluginUninstalledWithName": "{0} telah dinyahpasang",
+ "PluginUpdatedWithName": "{0} telah dikemaskini",
+ "ProviderValue": "Pembekal: {0}",
"ScheduledTaskFailedWithName": "{0} gagal",
"ScheduledTaskStartedWithName": "{0} bermula",
- "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
- "Shows": "Series",
+ "ServerNameNeedsToBeRestarted": "{0} perlu di ulangmula",
+ "Shows": "Tayangan",
"Songs": "Lagu-lagu",
"StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
- "Sync": "Sync",
+ "Sync": "Segerak",
"System": "Sistem",
- "TvShows": "TV Shows",
- "User": "User",
- "UserCreatedWithName": "User {0} has been created",
- "UserDeletedWithName": "User {0} has been deleted",
- "UserDownloadingItemWithValues": "{0} is downloading {1}",
+ "TvShows": "Tayangan TV",
+ "User": "Pengguna",
+ "UserCreatedWithName": "Pengguna {0} telah diwujudkan",
+ "UserDeletedWithName": "Pengguna {0} telah dipadamkan",
+ "UserDownloadingItemWithValues": "{0} sedang memuat turun {1}",
"UserLockedOutWithName": "Pengguna {0} telah dikunci",
"UserOfflineFromDevice": "{0} telah terputus dari {1}",
"UserOnlineFromDevice": "{0} berada dalam talian dari {1}",
"UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}",
"UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}",
- "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
- "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
- "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+ "UserStartedPlayingItemWithValues": "{0} sedang dimainkan {1} pada {2}",
+ "UserStoppedPlayingItemWithValues": "{0} telah tamat dimainkan {1} pada {2}",
+ "ValueHasBeenAddedToLibrary": "{0} telah ditambah ke media library anda",
"ValueSpecialEpisodeName": "Khas - {0}",
"VersionNumber": "Versi {0}",
"TaskCleanActivityLog": "Log Aktiviti Bersih",
@@ -101,5 +101,13 @@
"Forced": "Paksa",
"Default": "Asal",
"TaskCleanCache": "Bersihkan Direktori Cache",
- "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi."
+ "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.",
+ "TaskRefreshPeople": "Segarkan Orang",
+ "TaskCleanLogsDescription": "Padamkan fail log yang berumur lebih dari {0} hari.",
+ "TaskCleanLogs": "Bersihkan Direktotri Log",
+ "TaskRefreshLibraryDescription": "Imbas perpustakaan media untuk mencari fail-fail baru dan menyegarkan metadata.",
+ "TaskRefreshLibrary": "Imbas Perpustakaan Media",
+ "TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.",
+ "TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab",
+ "TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index fbe1f7c4d..317bdcfcb 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -118,5 +118,7 @@
"Undefined": "Udefinert",
"Forced": "Tvunget",
"Default": "Standard",
- "TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen."
+ "TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen.",
+ "TaskOptimizeDatabase": "Optimiser database",
+ "TaskOptimizeDatabaseDescription": "Komprimerer database og frigjør plass. Denne prosessen kan forbedre ytelsen etter skanning av bibliotek eller andre handlinger som fører til databaseendringer."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 8e820d40c..8584fc065 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -69,7 +69,7 @@
"UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ",
"UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ",
"User": "प्रयोगकर्ता",
- "PluginInstalledWithName": "",
+ "PluginInstalledWithName": "{0} सभएको थियो",
"StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
"Songs": "गीतहरू",
"Shows": "शोहरू",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 2973c8c6e..9d512dea1 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,7 +1,7 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Applicatie",
+ "Application": "Toepassing",
"Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
"Books": "Boeken",
@@ -15,7 +15,7 @@
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
- "HeaderAlbumArtists": "Albumartiesten",
+ "HeaderAlbumArtists": "Album Artiesten",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
- "HomeVideos": "Home video's",
+ "HomeVideos": "Thuis video's",
"Inherit": "Erven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -92,11 +92,11 @@
"ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
"ValueSpecialEpisodeName": "Speciaal - {0}",
"VersionNumber": "Versie {0}",
- "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar missende ondertitels gebaseerd op metadata configuratie.",
- "TaskDownloadMissingSubtitles": "Download missende ondertitels",
+ "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar missende ondertiteling gebaseerd op metadata configuratie.",
+ "TaskDownloadMissingSubtitles": "Download missende ondertiteling",
"TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.",
"TaskRefreshChannels": "Vernieuw Kanalen",
- "TaskCleanTranscodeDescription": "Verwijder transcode bestanden ouder dan 1 dag.",
+ "TaskCleanTranscodeDescription": "Verwijdert transcode bestanden ouder dan 1 dag.",
"TaskCleanLogs": "Log Folder Opschonen",
"TaskCleanTranscode": "Transcode Folder Opschonen",
"TaskUpdatePluginsDescription": "Download en installeert updates voor plugins waar automatisch updaten aan staat.",
@@ -108,15 +108,17 @@
"TaskRefreshLibrary": "Scan Media Bibliotheek",
"TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.",
"TaskRefreshChapterImages": "Hoofdstukafbeeldingen Uitpakken",
- "TaskCleanCacheDescription": "Verwijder gecachte bestanden die het systeem niet langer nodig heeft.",
+ "TaskCleanCacheDescription": "Verwijdert gecachte bestanden die het systeem niet langer nodig heeft.",
"TaskCleanCache": "Cache Folder Opschonen",
"TasksChannelsCategory": "Internet Kanalen",
"TasksApplicationCategory": "Applicatie",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
- "TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
+ "TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde tijd.",
"TaskCleanActivityLog": "Leeg activiteiten logboek",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
- "Default": "Standaard"
+ "Default": "Standaard",
+ "TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
+ "TaskOptimizeDatabase": "Database optimaliseren"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index d1db09232..4ac57b630 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -24,7 +24,7 @@
"TasksLibraryCategory": "ਲਾਇਬ੍ਰੇਰੀ",
"TasksMaintenanceCategory": "ਰੱਖ-ਰਖਾਅ",
"VersionNumber": "ਵਰਜਨ {0}",
- "ValueSpecialEpisodeName": "ਵਿਸ਼ੇਸ਼ - {0}",
+ "ValueSpecialEpisodeName": "ਖਾਸ - {0}",
"ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
"UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
@@ -43,8 +43,8 @@
"Sync": "ਸਿੰਕ",
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
"StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.",
- "Songs": "ਗਾਣੇ",
- "Shows": "ਸ਼ੋਅਜ਼",
+ "Songs": "ਗਾਣੇਂ",
+ "Shows": "ਸ਼ੋਅ",
"ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
"ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
"ScheduledTaskFailedWithName": "{0} ਅਸਫਲ",
@@ -53,7 +53,7 @@
"PluginUninstalledWithName": "{0} ਅਣਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ ਸੀ",
"PluginInstalledWithName": "{0} ਲਗਾਇਆ ਗਿਆ ਸੀ",
"Plugin": "ਪਲੱਗਇਨ",
- "Playlists": "ਪਲੇਲਿਸਟਸ",
+ "Playlists": "ਪਲੇਸੂਚੀਆਂ",
"Photos": "ਫੋਟੋਆਂ",
"NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
"NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
@@ -102,13 +102,13 @@
"HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
"Genres": "ਸ਼ੈਲੀਆਂ",
"Forced": "ਮਜਬੂਰ",
- "Folders": "ਫੋਲਡਰ",
+ "Folders": "ਫੋਲਡਰਸ",
"Favorites": "ਮਨਪਸੰਦ",
"FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}",
"DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
"DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
- "Default": "ਮੂਲ",
- "Collections": "ਸੰਗ੍ਰਹਿ",
+ "Default": "ਡਿਫੌਲਟ",
+ "Collections": "ਸੰਗ੍ਰਹਿਣ",
"ChapterNameValue": "ਅਧਿਆਇ {0}",
"Channels": "ਚੈਨਲ",
"CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index e3da96a85..4fa8d2bb4 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Telewizja",
"HeaderNextUp": "Do obejrzenia",
"HeaderRecordingGroups": "Grupy nagrań",
- "HomeVideos": "Nagrania prywatne",
+ "HomeVideos": "Nagrania domowe",
"Inherit": "Dziedzicz",
"ItemAddedWithName": "{0} zostało dodane do biblioteki",
"ItemRemovedWithName": "{0} zostało usunięte z biblioteki",
@@ -47,7 +47,7 @@
"NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji",
"NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację",
"NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki",
- "NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane",
+ "NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane",
"NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego",
"NotificationOptionInstallationFailed": "Nieudana instalacja",
"NotificationOptionNewLibraryContent": "Dodano nową zawartość",
@@ -98,7 +98,7 @@
"TaskRefreshChannels": "Odśwież kanały",
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
"TaskCleanTranscode": "Wyczyść folder transkodowania",
- "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.",
+ "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
"TaskUpdatePlugins": "Aktualizuj pluginy",
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
"TaskRefreshPeople": "Odśwież obsadę",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Czyść dziennik aktywności",
"Undefined": "Nieustalony",
"Forced": "Wymuszony",
- "Default": "Domyślne"
+ "Default": "Domyślne",
+ "TaskOptimizeDatabase": "Optymalizuj bazę danych",
+ "TaskOptimizeDatabaseDescription": "Kompaktuje bazę danych i obcina wolne miejsce. Uruchomienie tego zadania po przeskanowaniu biblioteki lub dokonaniu innych zmian, które pociągają za sobą modyfikacje bazy danych, może poprawić wydajność."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
new file mode 100644
index 000000000..81aa996d9
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -0,0 +1,7 @@
+{
+ "Books": "Libros",
+ "AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
+ "Artists": "Artistas",
+ "Songs": "Shantees",
+ "Albums": "Ships"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 323dcced0..be71289b1 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpar Registro de Atividades",
"Undefined": "Indefinido",
"Forced": "Forçado",
- "Default": "Padrão"
+ "Default": "Padrão",
+ "TaskOptimizeDatabaseDescription": "Compactar base de dados e liberar espaço livre. Executar esta tarefa após realizar mudanças que impliquem em modificações da base de dados pode trazer melhorias de desempenho.",
+ "TaskOptimizeDatabase": "Otimizar base de dados"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 8c41edf96..8870de168 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -15,7 +15,7 @@
"Favorites": "Favoritos",
"Folders": "Pastas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas do Álbum",
+ "HeaderAlbumArtists": "Álbum do Artista",
"HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
@@ -39,7 +39,7 @@
"MixedContent": "Conteúdo Misto",
"Movies": "Filmes",
"Music": "Música",
- "MusicVideos": "Videoclips",
+ "MusicVideos": "Videoclipes",
"NameInstallFailed": "{0} falha na instalação",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconhecida",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
- "Default": "Padrão"
+ "Default": "Padrão",
+ "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
+ "TaskOptimizeDatabase": "Otimizar base de dados"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index f1a78b2d3..a9dbd53ea 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -1,6 +1,6 @@
{
- "HeaderLiveTV": "TV em Directo",
- "Collections": "Colecções",
+ "HeaderLiveTV": "TV Ao Vivo",
+ "Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
"Albums": "Álbuns",
@@ -10,29 +10,29 @@
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
- "HeaderContinueWatching": "Continuar a Assistir",
+ "HeaderContinueWatching": "Continuar assistindo",
"HeaderAlbumArtists": "Artistas do Álbum",
- "Genres": "Géneros",
- "Folders": "Directórios",
+ "Genres": "Gêneros",
+ "Folders": "Diretórios",
"Favorites": "Favoritos",
"Channels": "Canais",
- "UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
+ "UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
"VersionNumber": "Versão {0}",
"ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
- "UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
- "UserPolicyUpdatedWithName": "A política do utilizador {0} foi alterada",
- "UserPasswordChangedWithName": "A palavra-passe do utilizador {0} foi alterada",
- "UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
+ "UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
+ "UserPolicyUpdatedWithName": "A política do usuário {0} foi alterada",
+ "UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
+ "UserOnlineFromDevice": "{0} está online a partir de {1}",
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
- "UserLockedOutWithName": "O utilizador {0} foi bloqueado",
- "UserDeletedWithName": "O utilizador {0} foi removido",
- "UserCreatedWithName": "O utilizador {0} foi criado",
- "User": "Utilizador",
+ "UserLockedOutWithName": "O usuário {0} foi bloqueado",
+ "UserDeletedWithName": "O usuário {0} foi removido",
+ "UserCreatedWithName": "O usuário {0} foi criado",
+ "User": "Usuário",
"TvShows": "Séries",
"System": "Sistema",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
- "StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente dentro de momentos.",
+ "StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciado",
"ScheduledTaskStartedWithName": "{0} iniciou",
"ScheduledTaskFailedWithName": "{0} falhou",
@@ -43,38 +43,38 @@
"Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
- "NotificationOptionUserLockedOut": "Utilizador bloqueado",
- "NotificationOptionTaskFailed": "Falha em tarefa agendada",
+ "NotificationOptionUserLockedOut": "Usuário bloqueado",
+ "NotificationOptionTaskFailed": "Falha na tarefa agendada",
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
- "NotificationOptionPluginUpdateInstalled": "Plugin actualizado",
+ "NotificationOptionPluginUpdateInstalled": "Plugin atualizado",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
"NotificationOptionPluginInstalled": "Plugin instalado",
"NotificationOptionPluginError": "Falha no plugin",
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
"NotificationOptionInstallationFailed": "Falha de instalação",
- "NotificationOptionCameraImageUploaded": "Imagem de câmara enviada",
+ "NotificationOptionCameraImageUploaded": "Imagem de câmera enviada",
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
- "NotificationOptionApplicationUpdateInstalled": "A actualização da aplicação foi instalada",
- "NotificationOptionApplicationUpdateAvailable": "Uma actualização da aplicação está disponível",
- "NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para transferência.",
+ "NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
+ "NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
+ "NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
"NameSeasonUnknown": "Temporada Desconhecida",
"NameSeasonNumber": "Temporada {0}",
"NameInstallFailed": "Falha na instalação de {0}",
- "MusicVideos": "Videoclips",
+ "MusicVideos": "Videoclipes",
"Music": "Música",
- "MixedContent": "Conteúdo Misto",
- "MessageServerConfigurationUpdated": "A configuração do servidor foi actualizada",
- "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na secção {0} foram atualizadas",
+ "MixedContent": "Conteúdo diverso",
+ "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
+ "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na seção {0} foram atualizadas",
"MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}",
- "MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
+ "MessageApplicationUpdated": "O servidor Jellyfin foi atualizado",
"Latest": "Mais Recente",
"LabelRunningTimeValue": "Duração: {0}",
"LabelIpAddressValue": "Endereço de IP: {0}",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"Inherit": "Herdar",
- "HomeVideos": "Vídeos Caseiros",
+ "HomeVideos": "Vídeos principais",
"HeaderRecordingGroups": "Grupos de Gravação",
"ValueSpecialEpisodeName": "Episódio Especial - {0}",
"Sync": "Sincronização",
@@ -83,22 +83,22 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
- "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
- "DeviceOnlineWithName": "{0} está connectado",
+ "FailedLoginAttemptWithUserName": "Tentativa falha de login a partir de {0}",
+ "DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} desconectou-se",
"ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
- "Application": "Aplicação",
- "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
+ "Application": "Aplicativo",
+ "AppDeviceValues": "Aplicativo {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
- "TasksApplicationCategory": "Aplicação",
+ "TasksApplicationCategory": "Aplicativo",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
"TaskUpdatePlugins": "Atualizar Plugins",
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
- "TaskCleanLogs": "Limpar diretório de log",
+ "TaskCleanLogs": "Limpar diretório de logs",
"TaskRefreshLibrary": "Escanear biblioteca de mídias",
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
@@ -109,13 +109,15 @@
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
"TaskCleanTranscode": "Limpar o diretório de Transcode",
- "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
+ "TaskUpdatePluginsDescription": "Baixa e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
"TaskRefreshPeople": "Atualizar pessoas",
- "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados.",
- "TaskCleanActivityLog": "Limpar registo de atividade",
+ "TaskRefreshLibraryDescription": "Pesquisa sua biblioteca de media por novos arquivos e atualiza os metadados.",
+ "TaskCleanActivityLog": "Limpar registro de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
- "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado."
+ "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
+ "TaskOptimizeDatabase": "Otimizar base de dados",
+ "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 510aac11c..f8fad7b63 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -74,7 +74,7 @@
"HeaderFavoriteArtists": "Artiști Favoriți",
"HeaderFavoriteAlbums": "Albume Favorite",
"HeaderContinueWatching": "Vizionează în continuare",
- "HeaderAlbumArtists": "Album Artiști",
+ "HeaderAlbumArtists": "Albume Artiști",
"Genres": "Genuri",
"Folders": "Dosare",
"Favorites": "Favorite",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index e58f8c39d..2d7163275 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -25,13 +25,13 @@
"HeaderLiveTV": "Эфир",
"HeaderNextUp": "Очередное",
"HeaderRecordingGroups": "Группы записей",
- "HomeVideos": "Домашнее видео",
+ "HomeVideos": "Домашние видео",
"Inherit": "Наследуемое",
"ItemAddedWithName": "{0} - добавлено в медиатеку",
"ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
- "Latest": "Последнее",
+ "Latest": "Крайнее",
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Очистить журнал активности",
"Undefined": "Не определено",
"Forced": "Форсир-ые",
- "Default": "По умолчанию"
+ "Default": "По умолчанию",
+ "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
+ "TaskOptimizeDatabase": "Оптимизировать базу данных"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 99fbd3954..37da7d5ab 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Vyčistiť log aktivít",
"Undefined": "Nedefinované",
"Forced": "Vynútené",
- "Default": "Predvolené"
+ "Default": "Predvolené",
+ "TaskOptimizeDatabaseDescription": "Zmenší databázu a odstráni prázdne miesto. Spustenie tejto úlohy po skenovaní knižnice alebo po iných zmenách zahŕňajúcich úpravy databáze môže zlepšiť výkon.",
+ "TaskOptimizeDatabase": "Optimalizovať databázu"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 343e067b7..a6fcbd3e2 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -16,7 +16,7 @@
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
- "HeaderContinueWatching": "Nadaljuj z ogledom",
+ "HeaderContinueWatching": "Nadaljuj ogled",
"HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Priljubljene epizode",
@@ -90,7 +90,7 @@
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
- "ValueSpecialEpisodeName": "Posebna - {0}",
+ "ValueSpecialEpisodeName": "Bonus - {0}",
"VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Počisti dnevnik aktivnosti",
"Undefined": "Nedoločen",
"Forced": "Prisilno",
- "Default": "Privzeto"
+ "Default": "Privzeto",
+ "TaskOptimizeDatabaseDescription": "Stisne bazo podatkov in uredi prazen prostor. Zagon tega opravila po iskanju predstavnosti ali drugih spremembah ki vplivajo na bazo podatkov lahko izboljša hitrost delovanja.",
+ "TaskOptimizeDatabase": "Optimiziraj bazo podatkov"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 0d909b06e..2766dab06 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -42,8 +42,8 @@
"Sync": "Sinkronizo",
"SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
"StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
- "Songs": "Këngë",
- "Shows": "Seriale",
+ "Songs": "Këngët",
+ "Shows": "Serialet",
"ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
"ScheduledTaskStartedWithName": "{0} filloi",
"ScheduledTaskFailedWithName": "{0} dështoi",
@@ -74,9 +74,9 @@
"NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi",
- "MusicVideos": "Video muzikore",
+ "MusicVideos": "Video Muzikore",
"Music": "Muzikë",
- "Movies": "Filma",
+ "Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier",
"MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
"MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
@@ -97,20 +97,27 @@
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
"HeaderAlbumArtists": "Artistët e albumeve",
- "Genres": "Zhanre",
- "Folders": "Dosje",
- "Favorites": "Të preferuara",
+ "Genres": "Zhanret",
+ "Folders": "Skedarët",
+ "Favorites": "Të preferuarat",
"FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
"DeviceOnlineWithName": "{0} u lidh",
"DeviceOfflineWithName": "{0} u shkëput",
- "Collections": "Koleksione",
+ "Collections": "Koleksionet",
"ChapterNameValue": "Kapituj",
- "Channels": "Kanale",
+ "Channels": "Kanalet",
"CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
- "Books": "Libra",
+ "Books": "Librat",
"AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
- "Artists": "Artistë",
+ "Artists": "Artistët",
"Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
- "Albums": "Albume"
+ "Albums": "Albumet",
+ "TaskCleanActivityLogDescription": "Pastro të dhënat mbi aktivitetin më të vjetra sesa koha e përcaktuar.",
+ "TaskCleanActivityLog": "Pastro të dhënat mbi aktivitetin",
+ "Undefined": "I papërcaktuar",
+ "Forced": "I detyruar",
+ "Default": "Parazgjedhur",
+ "TaskOptimizeDatabaseDescription": "Kompakton bazën e të dhënave dhe shkurton hapësirën e lirë. Drejtimi i kësaj detyre pasi skanoni bibliotekën ose bëni ndryshime të tjera që nënkuptojnë modifikime të bazës së të dhënave mund të përmirësojë performancën.",
+ "TaskOptimizeDatabase": "Optimizo databazën"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 15fb34186..e31208e80 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -50,7 +50,7 @@
"NameSeasonUnknown": "Непозната сезона",
"NameSeasonNumber": "Сезона {0}",
"NameInstallFailed": "Инсталација {0} није успела",
- "MusicVideos": "Музички спотови",
+ "MusicVideos": "Музички видео",
"Music": "Музика",
"Movies": "Филмови",
"MixedContent": "Мешовит садржај",
@@ -64,7 +64,7 @@
"ItemRemovedWithName": "{0} уклоњено из библиотеке",
"ItemAddedWithName": "{0} додато у библиотеку",
"Inherit": "Наследи",
- "HomeVideos": "Кућни видео",
+ "HomeVideos": "Кућни Видео",
"HeaderRecordingGroups": "Групе снимања",
"HeaderNextUp": "Следи",
"HeaderLiveTV": "ТВ уживо",
@@ -117,5 +117,6 @@
"TaskCleanActivityLog": "Очисти историју активности",
"Undefined": "Недефинисано",
"Forced": "Принудно",
- "Default": "Подразумевано"
+ "Default": "Подразумевано",
+ "TaskOptimizeDatabase": "Оптимизуј датабазу"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index d992bf79b..f3f601661 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -15,7 +15,7 @@
"Favorites": "Favoriter",
"Folders": "Mappar",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albumartister",
+ "HeaderAlbumArtists": "Albumsartister",
"HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Nästa",
"HeaderRecordingGroups": "Inspelningsgrupper",
- "HomeVideos": "Hemvideor",
+ "HomeVideos": "Hemmavideor",
"Inherit": "Ärv",
"ItemAddedWithName": "{0} lades till i biblioteket",
"ItemRemovedWithName": "{0} togs bort från biblioteket",
@@ -96,8 +96,8 @@
"TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter",
"TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.",
"TaskRefreshChannels": "Uppdatera kanaler",
- "TaskCleanTranscodeDescription": "Raderar transkodningsfiler som är mer än en dag gamla.",
- "TaskCleanTranscode": "Töm transkodningskatalog",
+ "TaskCleanTranscodeDescription": "Raderar omkodningsfiler som är mer än en dag gamla.",
+ "TaskCleanTranscode": "Töm omkodningskatalog",
"TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.",
"TaskUpdatePlugins": "Uppdatera insticksprogram",
"TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.",
@@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Rensa Aktivitets Logg",
"Undefined": "odefinierad",
"Forced": "Tvingad",
- "Default": "Standard"
+ "Default": "Standard",
+ "TaskOptimizeDatabase": "Optimera databasen",
+ "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 129986ed0..98d763fcd 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -21,7 +21,7 @@
"Inherit": "மரபுரிமையாகப் பெறு",
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
"Folders": "கோப்புறைகள்",
- "FailedLoginAttemptWithUserName": "{0} இல் இருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
+ "FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது",
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்",
@@ -85,7 +85,7 @@
"HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
"HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்",
"HeaderContinueWatching": "தொடர்ந்து பார்",
- "HeaderAlbumArtists": "இசைக் கலைஞர்கள்",
+ "HeaderAlbumArtists": "கலைஞரின் ஆல்பம்",
"Genres": "வகைகள்",
"Favorites": "பிடித்தவை",
"ChapterNameValue": "அத்தியாயம் {0}",
@@ -117,5 +117,7 @@
"TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி",
"Undefined": "வரையறுக்கப்படாத",
"Forced": "கட்டாயப்படுத்தப்பட்டது",
- "Default": "இயல்புநிலை"
+ "Default": "இயல்புநிலை",
+ "TaskOptimizeDatabaseDescription": "தரவுத்தளத்தை சுருக்கி, இலவச இடத்தை குறைக்கிறது. நூலகத்தை ஸ்கேன் செய்தபின் அல்லது தரவுத்தள மாற்றங்களைக் குறிக்கும் பிற மாற்றங்களைச் செய்தபின் இந்த பணியை இயக்குவது செயல்திறனை மேம்படுத்தக்கூடும்.",
+ "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்"
}
diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json
new file mode 100644
index 000000000..a9a8ceae0
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/te.json
@@ -0,0 +1,23 @@
+{
+ "ValueSpecialEpisodeName": "ప్రత్యేక - {0}",
+ "Sync": "సమకాలీకరించు",
+ "Songs": "పాటలు",
+ "Shows": "ప్రదర్శనలు",
+ "Playlists": "ప్లేజాబితాలు",
+ "Photos": "ఫోటోలు",
+ "MusicVideos": "మ్యూజిక్ వీడియోలు",
+ "Music": "సంగీతం",
+ "Movies": "సినిమాలు",
+ "HeaderContinueWatching": "చూడటం కొనసాగించండి",
+ "HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు",
+ "Genres": "శైలులు",
+ "Forced": "బలవంతంగా",
+ "Folders": "ఫోల్డర్లు",
+ "Favorites": "ఇష్టమైనవి",
+ "Default": "డిఫాల్ట్",
+ "Collections": "సేకరణలు",
+ "Channels": "ఛానెల్‌లు",
+ "Books": "పుస్తకాలు",
+ "Artists": "కళాకారులు",
+ "Albums": "ఆల్బమ్‌లు"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index c6b904045..8fadb88ac 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}",
- "Collections": "Koleksiyon",
+ "Collections": "Koleksiyonlar",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları",
- "HomeVideos": "Ev videoları",
+ "HomeVideos": "Ana sayfa videoları",
"Inherit": "Devral",
"ItemAddedWithName": "{0} kütüphaneye eklendi",
"ItemRemovedWithName": "{0} kütüphaneden silindi",
@@ -43,7 +43,7 @@
"NameInstallFailed": "{0} kurulumu başarısız",
"NameSeasonNumber": "Sezon {0}",
"NameSeasonUnknown": "Bilinmeyen Sezon",
- "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir versiyonu indirmek için hazır.",
+ "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
"NotificationOptionApplicationUpdateInstalled": "Uygulama güncellemesi yüklendi",
"NotificationOptionAudioPlayback": "Ses çalma başladı",
@@ -75,7 +75,7 @@
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
- "Sync": "Eşitle",
+ "Sync": "Eşzamanlama",
"System": "Sistem",
"TvShows": "Diziler",
"User": "Kullanıcı",
@@ -89,34 +89,36 @@
"UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
- "ValueHasBeenAddedToLibrary": "Medya kitaplığınıza {0} eklendi",
+ "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
"ValueSpecialEpisodeName": "Özel - {0}",
- "VersionNumber": "Versiyon {0}",
+ "VersionNumber": "Sürüm {0}",
"TaskCleanCache": "Geçici dosya klasörünü temizle",
"TasksChannelsCategory": "İnternet kanalları",
"TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane",
- "TasksMaintenanceCategory": "Onarım",
+ "TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
"TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile",
- "TaskCleanTranscodeDescription": "Bir günü dolmuş dönüştürme bilgisi içeren dosyaları siler.",
+ "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
"TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
"TaskUpdatePlugins": "Eklentileri Güncelle",
"TaskRefreshPeople": "Kullanıcıları Yenile",
- "TaskCleanLogsDescription": "{0} günden eski log dosyalarını siler.",
- "TaskCleanLogs": "Log Dizinini Temizle",
- "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve bilgileri yeniler.",
+ "TaskCleanLogsDescription": "{0} günden eski günlük dosyalarını siler.",
+ "TaskCleanLogs": "Günlük Dizinini Temizle",
+ "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.",
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
- "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
- "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
+ "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle",
+ "TaskCleanActivityLogDescription": "Yapılandırılan tarihten daha eski olan etkinlik günlüğü girişlerini siler.",
"Undefined": "Bilinmeyen",
"Default": "Varsayılan",
- "Forced": "Zorla"
+ "Forced": "Zorla",
+ "TaskOptimizeDatabaseDescription": "Veritabanını sıkıştırır ve boş alanı keser. Kitaplığı taradıktan sonra veya veritabanında değişiklik anlamına gelen diğer işlemleri yaptıktan sonra bu görevi çalıştırmak performansı artırabilir.",
+ "TaskOptimizeDatabase": "Veritabanını optimize et"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 5a2069df5..1c7d73615 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -1,5 +1,5 @@
{
- "MusicVideos": "Музичні відеокліпи",
+ "MusicVideos": "Відеокліпи",
"Music": "Музика",
"Movies": "Фільми",
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
@@ -38,7 +38,7 @@
"NotificationOptionPluginInstalled": "Плагін встановлено",
"NotificationOptionPluginError": "Помилка плагіна",
"NotificationOptionNewLibraryContent": "Додано новий контент",
- "HomeVideos": "Домашнє відео",
+ "HomeVideos": "Мої відео",
"FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
"LabelRunningTimeValue": "Тривалість: {0}",
"TaskDownloadMissingSubtitlesDescription": "Шукає в Інтернеті відсутні субтитри на основі конфігурації метаданих.",
@@ -117,5 +117,7 @@
"TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.",
"TaskCleanActivityLog": "Очистити журнал активності",
"Undefined": "Не визначено",
- "Default": "За замовчуванням"
+ "Default": "За замовчуванням",
+ "TaskOptimizeDatabase": "Оптимізувати базу даних",
+ "TaskOptimizeDatabaseDescription": "Стиснення бази даних та збільшення вільного простору. Виконання цього завдання після сканування бібліотеки або внесення інших змін, які передбачають модифікацію бази даних, може покращити продуктивність."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 5d6d0775c..11af9fc98 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -90,7 +90,7 @@
"NameSeasonUnknown": "نامعلوم باب",
"NameSeasonNumber": "باب {0}",
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
- "MusicVideos": "موسیقی ویڈیو",
+ "MusicVideos": "ویڈیو موسیقی",
"Music": "موسیقی",
"MixedContent": "مخلوط مواد",
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
@@ -99,18 +99,19 @@
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
"Latest": "تازہ ترین",
"LabelRunningTimeValue": "چلانے کی مدت",
- "LabelIpAddressValue": "ای پی پتے {0}",
+ "LabelIpAddressValue": "آئ پی ایڈریس {0}",
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
- "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
+ "FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
"ChapterNameValue": "باب",
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
"Application": "پروگرام",
- "AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
+ "AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
+ "Forced": "جَبری"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 58652c469..b7ece8d5f 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -3,18 +3,18 @@
"Favorites": "Yêu Thích",
"Folders": "Thư Mục",
"Genres": "Thể Loại",
- "HeaderAlbumArtists": "Tuyển Tập Nghệ sĩ",
+ "HeaderAlbumArtists": "Album nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim",
"Photos": "Ảnh",
"Playlists": "Danh sách phát",
"Shows": "Chương Trình TV",
- "Songs": "Các Bài Hát",
+ "Songs": "Bài Hát",
"Sync": "Đồng Bộ",
"ValueSpecialEpisodeName": "Đặc Biệt - {0}",
"Albums": "Tuyển Tập",
- "Artists": "Các Nghệ Sĩ",
+ "Artists": "Ca Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
@@ -32,7 +32,7 @@
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
- "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+ "TaskCleanCache": "Làm Sạch Thư Mục Bộ Nhớ Đệm",
"TasksChannelsCategory": "Kênh Internet",
"TasksApplicationCategory": "Ứng Dụng",
"TasksLibraryCategory": "Thư Viện",
@@ -62,11 +62,11 @@
"PluginUninstalledWithName": "{0} đã được gỡ bỏ",
"PluginInstalledWithName": "{0} đã được cài đặt",
"Plugin": "Plugin",
- "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
+ "NotificationOptionVideoPlaybackStopped": "Đã dừng phát lại video",
"NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
"NotificationOptionUserLockedOut": "Người dùng bị khóa",
"NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
- "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+ "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại máy chủ",
"NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
"NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
"NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
@@ -75,14 +75,14 @@
"NotificationOptionInstallationFailed": "Cài đặt thất bại",
"NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
"NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
- "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+ "NotificationOptionAudioPlayback": "Đã bắt đầu phát lại âm thanh",
"NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
"NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
"NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
"NameSeasonUnknown": "Không Rõ Mùa",
"NameSeasonNumber": "Phần {0}",
"NameInstallFailed": "{0} cài đặt thất bại",
- "MusicVideos": "Video Nhạc",
+ "MusicVideos": "Videos Nhạc",
"Music": "Nhạc",
"MixedContent": "Nội dung hỗn hợp",
"MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
@@ -95,7 +95,7 @@
"ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
"ItemAddedWithName": "{0} được thêm vào thư viện",
"Inherit": "Thừa hưởng",
- "HomeVideos": "Video nhà",
+ "HomeVideos": "Video Nhà",
"HeaderRecordingGroups": "Nhóm Ghi Video",
"HeaderNextUp": "Tiếp Theo",
"HeaderFavoriteSongs": "Bài Hát Yêu Thích",
@@ -103,7 +103,7 @@
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
"HeaderFavoriteAlbums": "Album Ưa Thích",
- "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
+ "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
"DeviceOnlineWithName": "{0} đã kết nối",
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
"ChapterNameValue": "Phân Cảnh {0}",
@@ -117,5 +117,7 @@
"TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động",
"Undefined": "Không Xác Định",
"Forced": "Bắt Buộc",
- "Default": "Mặc Định"
+ "Default": "Mặc Định",
+ "TaskOptimizeDatabaseDescription": "Thu gọn cơ sở dữ liệu và cắt bớt dung lượng trống. Chạy tác vụ này sau khi quét thư viện hoặc thực hiện các thay đổi khác ngụ ý sửa đổi cơ sở dữ liệu có thể cải thiện hiệu suất.",
+ "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 12803456e..ac4eb644b 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -7,16 +7,16 @@
"Books": "书籍",
"CameraImageUploadedFrom": "新的相机图像已从 {0} 上传",
"Channels": "频道",
- "ChapterNameValue": "第 {0} 集",
+ "ChapterNameValue": "章节 {0}",
"Collections": "合集",
"DeviceOfflineWithName": "{0} 已断开",
"DeviceOnlineWithName": "{0} 已连接",
- "FailedLoginAttemptWithUserName": "来自 {0} 的失败登入",
+ "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
"Favorites": "我的最爱",
"Folders": "文件夹",
"Genres": "风格",
- "HeaderAlbumArtists": "专辑作家",
- "HeaderContinueWatching": "继续观影",
+ "HeaderAlbumArtists": "专辑艺术家",
+ "HeaderContinueWatching": "继续观看",
"HeaderFavoriteAlbums": "收藏的专辑",
"HeaderFavoriteArtists": "最爱的艺术家",
"HeaderFavoriteEpisodes": "最爱的剧集",
@@ -108,8 +108,8 @@
"TaskCleanLogs": "清理日志目录",
"TaskRefreshLibraryDescription": "扫描你的媒体库以获取新文件并刷新元数据。",
"TaskRefreshLibrary": "扫描媒体库",
- "TaskRefreshChapterImagesDescription": "为包含剧集的视频提取缩略图。",
- "TaskRefreshChapterImages": "提取剧集图片",
+ "TaskRefreshChapterImagesDescription": "为包含章节的视频提取缩略图。",
+ "TaskRefreshChapterImages": "提取章节图片",
"TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
"TaskCleanCache": "清理缓存目录",
"TasksApplicationCategory": "应用程序",
@@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。",
"Undefined": "未定义",
"Forced": "强制的",
- "Default": "默认"
+ "Default": "默认",
+ "TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
+ "TaskOptimizeDatabase": "优化数据库"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 3dad21dcb..1cc97bc27 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -13,7 +13,7 @@
"DeviceOnlineWithName": "{0} 已經連接",
"FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
"Favorites": "我的最愛",
- "Folders": "檔案夾",
+ "Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
"HeaderContinueWatching": "繼續觀看",
@@ -39,7 +39,7 @@
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
- "MusicVideos": "音樂視頻",
+ "MusicVideos": "音樂影片",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
@@ -117,5 +117,8 @@
"TaskCleanActivityLog": "清理活動記錄",
"Undefined": "未定義",
"Forced": "強制",
- "Default": "預設"
+ "Default": "預設",
+ "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
+ "TaskOptimizeDatabase": "最佳化數據庫",
+ "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index c3b223f63..585d81450 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -24,7 +24,7 @@
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
- "HomeVideos": "自製影片",
+ "HomeVideos": "家庭影片",
"ItemAddedWithName": "{0} 已新增至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
"LabelIpAddressValue": "IP 位址:{0}",
@@ -117,5 +117,7 @@
"TaskCleanActivityLog": "清除活動紀錄",
"Undefined": "未定義的",
"Forced": "強制",
- "Default": "原本"
+ "Default": "原本",
+ "TaskOptimizeDatabaseDescription": "縮小資料庫並釋放可用空間。在掃描資料庫或進行資料庫相關的更動後使用此功能會增加效能。",
+ "TaskOptimizeDatabase": "最佳化資料庫"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json
new file mode 100644
index 000000000..b5f4b920f
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/zu.json
@@ -0,0 +1,29 @@
+{
+ "TasksApplicationCategory": "Ukusetshenziswa",
+ "TasksLibraryCategory": "Umtapo",
+ "TasksMaintenanceCategory": "Ukunakekela",
+ "User": "Umsebenzisi",
+ "Undefined": "Akuchaziwe",
+ "System": "Isistimu",
+ "Sync": "Vumelanisa",
+ "Songs": "Amaculo",
+ "Shows": "Izinhlelo",
+ "Plugin": "Isijobelelo",
+ "Playlists": "Izinhla Zokudlalayo",
+ "Photos": "Izithombe",
+ "Music": "Umculo",
+ "Movies": "Amamuvi",
+ "Latest": "lwakamuva",
+ "Inherit": "Ngefa",
+ "Forced": "Kuphoqiwe",
+ "Application": "Ukusetshenziswa",
+ "Genres": "Izinhlobo",
+ "Folders": "Izikhwama",
+ "Favorites": "Izintandokazi",
+ "Default": "Okumisiwe",
+ "Collections": "Amaqoqo",
+ "Channels": "Amashaneli",
+ "Books": "Izincwadi",
+ "Artists": "Abadlali",
+ "Albums": "Ama-albhamu"
+}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index b1ff28c2c..dbd70342a 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -8,8 +6,8 @@ using System.IO;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -23,6 +21,9 @@ namespace Emby.Server.Implementations.Localization
public class LocalizationManager : ILocalizationManager
{
private const string DefaultCulture = "en-US";
+ private const string RatingsPath = "Emby.Server.Implementations.Localization.Ratings.";
+ private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
+ private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
@@ -35,10 +36,10 @@ namespace Emby.Server.Implementations.Localization
private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
- private List<CultureDto> _cultures;
-
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private List<CultureDto> _cultures = new List<CultureDto>();
+
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
/// </summary>
@@ -58,43 +59,39 @@ namespace Emby.Server.Implementations.Localization
/// <returns><see cref="Task" />.</returns>
public async Task LoadAll()
{
- const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings.";
-
// Extract from the assembly
foreach (var resource in _assembly.GetManifestResourceNames())
{
- if (!resource.StartsWith(RatingsResource, StringComparison.Ordinal))
+ if (!resource.StartsWith(RatingsPath, StringComparison.Ordinal))
{
continue;
}
- string countryCode = resource.Substring(RatingsResource.Length, 2);
+ string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
- using (var str = _assembly.GetManifestResourceStream(resource))
- using (var reader = new StreamReader(str))
+ await using var stream = _assembly.GetManifestResourceStream(resource);
+ using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ if (string.IsNullOrWhiteSpace(line))
{
- if (string.IsNullOrWhiteSpace(line))
- {
- continue;
- }
-
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- var name = parts[0];
- dict.Add(name, new ParentalRating(name, value));
- }
+ continue;
+ }
+
+ string[] parts = line.Split(',');
+ if (parts.Length == 2
+ && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ var name = parts[0];
+ dict.Add(name, new ParentalRating(name, value));
+ }
#if DEBUG
- else
- {
- _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
- }
-#endif
+ else
+ {
+ _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
+#endif
}
_allParentalRatings[countryCode] = dict;
@@ -114,52 +111,49 @@ namespace Emby.Server.Implementations.Localization
{
List<CultureDto> list = new List<CultureDto>();
- const string ResourcePath = "Emby.Server.Implementations.Localization.iso6392.txt";
-
- using (var stream = _assembly.GetManifestResourceStream(ResourcePath))
- using (var reader = new StreamReader(stream))
+ await using var stream = _assembly.GetManifestResourceStream(CulturesPath)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
+ using var reader = new StreamReader(stream);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ if (string.IsNullOrWhiteSpace(line))
{
- if (string.IsNullOrWhiteSpace(line))
+ continue;
+ }
+
+ var parts = line.Split('|');
+
+ if (parts.Length == 5)
+ {
+ string name = parts[3];
+ if (string.IsNullOrWhiteSpace(name))
{
continue;
}
- var parts = line.Split('|');
+ string twoCharName = parts[2];
+ if (string.IsNullOrWhiteSpace(twoCharName))
+ {
+ continue;
+ }
- if (parts.Length == 5)
+ string[] threeletterNames;
+ if (string.IsNullOrWhiteSpace(parts[1]))
{
- string name = parts[3];
- if (string.IsNullOrWhiteSpace(name))
- {
- continue;
- }
-
- string twoCharName = parts[2];
- if (string.IsNullOrWhiteSpace(twoCharName))
- {
- continue;
- }
-
- string[] threeletterNames;
- if (string.IsNullOrWhiteSpace(parts[1]))
- {
- threeletterNames = new[] { parts[0] };
- }
- else
- {
- threeletterNames = new[] { parts[0], parts[1] };
- }
-
- list.Add(new CultureDto
- {
- DisplayName = name,
- Name = name,
- ThreeLetterISOLanguageNames = threeletterNames,
- TwoLetterISOLanguageName = twoCharName
- });
+ threeletterNames = new[] { parts[0] };
}
+ else
+ {
+ threeletterNames = new[] { parts[0], parts[1] };
+ }
+
+ list.Add(new CultureDto
+ {
+ DisplayName = name,
+ Name = name,
+ ThreeLetterISOLanguageNames = threeletterNames,
+ TwoLetterISOLanguageName = twoCharName
+ });
}
}
@@ -167,7 +161,7 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
- public CultureDto FindLanguageInfo(string language)
+ public CultureDto? FindLanguageInfo(string language)
{
// TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
for (var i = 0; i < _cultures.Count; i++)
@@ -188,9 +182,10 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<CountryInfo> GetCountries()
{
- using StreamReader reader = new StreamReader(_assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json"));
-
- return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions);
+ using StreamReader reader = new StreamReader(
+ _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"));
+ return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions)
+ ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'");
}
/// <inheritdoc />
@@ -210,7 +205,9 @@ namespace Emby.Server.Implementations.Localization
countryCode = "us";
}
- return GetRatings(countryCode) ?? GetRatings("us");
+ return GetRatings(countryCode)
+ ?? GetRatings("us")
+ ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
}
/// <summary>
@@ -218,7 +215,7 @@ namespace Emby.Server.Implementations.Localization
/// </summary>
/// <param name="countryCode">The country code.</param>
/// <returns>The ratings.</returns>
- private Dictionary<string, ParentalRating> GetRatings(string countryCode)
+ private Dictionary<string, ParentalRating>? GetRatings(string countryCode)
{
_allParentalRatings.TryGetValue(countryCode, out var value);
@@ -243,7 +240,7 @@ namespace Emby.Server.Implementations.Localization
var ratingsDictionary = GetParentalRatingsDictionary();
- if (ratingsDictionary.TryGetValue(rating, out ParentalRating value))
+ if (ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
{
return value.Value;
}
@@ -274,20 +271,6 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
- public bool HasUnicodeCategory(string value, UnicodeCategory category)
- {
- foreach (var chr in value)
- {
- if (char.GetUnicodeCategory(chr) == category)
- {
- return true;
- }
- }
-
- return false;
- }
-
- /// <inheritdoc />
public string GetLocalizedString(string phrase)
{
return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture);
@@ -327,7 +310,7 @@ namespace Emby.Server.Implementations.Localization
return _dictionaries.GetOrAdd(
culture,
- (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
+ static (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
this);
}
@@ -350,22 +333,23 @@ namespace Emby.Server.Implementations.Localization
private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
{
- using (var stream = _assembly.GetManifestResourceStream(resourcePath))
+ await using var stream = _assembly.GetManifestResourceStream(resourcePath);
+ // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
+ if (stream == null)
{
- // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
- if (stream != null)
- {
- var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+ _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
+ return;
+ }
- foreach (var key in dict.Keys)
- {
- dictionary[key] = dict[key];
- }
- }
- else
- {
- _logger.LogError("Missing translation/culture resource: {ResourcePath}", resourcePath);
- }
+ var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
+ if (dict == null)
+ {
+ throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
+ }
+
+ foreach (var key in dict.Keys)
+ {
+ dictionary[key] = dict[key];
}
}
@@ -388,43 +372,76 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<LocalizationOption> GetLocalizationOptions()
{
- yield return new LocalizationOption("Arabic", "ar");
- yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG");
- yield return new LocalizationOption("Catalan", "ca");
- yield return new LocalizationOption("Chinese Simplified", "zh-CN");
- yield return new LocalizationOption("Chinese Traditional", "zh-TW");
- yield return new LocalizationOption("Croatian", "hr");
- yield return new LocalizationOption("Czech", "cs");
- yield return new LocalizationOption("Danish", "da");
- yield return new LocalizationOption("Dutch", "nl");
+ yield return new LocalizationOption("Afrikaans", "af");
+ yield return new LocalizationOption("العربية", "ar");
+ yield return new LocalizationOption("Беларуская", "be");
+ yield return new LocalizationOption("Български", "bg-BG");
+ yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn");
+ yield return new LocalizationOption("Català", "ca");
+ yield return new LocalizationOption("Čeština", "cs");
+ yield return new LocalizationOption("Cymraeg", "cy");
+ yield return new LocalizationOption("Dansk", "da");
+ yield return new LocalizationOption("Deutsch", "de");
yield return new LocalizationOption("English (United Kingdom)", "en-GB");
- yield return new LocalizationOption("English (United States)", "en-US");
- yield return new LocalizationOption("French", "fr");
- yield return new LocalizationOption("French (Canada)", "fr-CA");
- yield return new LocalizationOption("German", "de");
- yield return new LocalizationOption("Greek", "el");
- yield return new LocalizationOption("Hebrew", "he");
- yield return new LocalizationOption("Hungarian", "hu");
- yield return new LocalizationOption("Italian", "it");
- yield return new LocalizationOption("Kazakh", "kk");
- yield return new LocalizationOption("Korean", "ko");
- yield return new LocalizationOption("Lithuanian", "lt-LT");
- yield return new LocalizationOption("Malay", "ms");
- yield return new LocalizationOption("Norwegian Bokmål", "nb");
- yield return new LocalizationOption("Persian", "fa");
- yield return new LocalizationOption("Polish", "pl");
- yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR");
- yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT");
- yield return new LocalizationOption("Russian", "ru");
- yield return new LocalizationOption("Slovak", "sk");
- yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI");
- yield return new LocalizationOption("Spanish", "es");
- yield return new LocalizationOption("Spanish (Argentina)", "es-AR");
- yield return new LocalizationOption("Spanish (Mexico)", "es-MX");
- yield return new LocalizationOption("Swedish", "sv");
- yield return new LocalizationOption("Swiss German", "gsw");
- yield return new LocalizationOption("Turkish", "tr");
+ yield return new LocalizationOption("English", "en-US");
+ yield return new LocalizationOption("Ελληνικά", "el");
+ yield return new LocalizationOption("Esperanto", "eo");
+ yield return new LocalizationOption("Español", "es");
+ yield return new LocalizationOption("Español americano", "es_419");
+ yield return new LocalizationOption("Español (Argentina)", "es-AR");
+ yield return new LocalizationOption("Español (Dominicana)", "es_DO");
+ yield return new LocalizationOption("Español (México)", "es-MX");
+ yield return new LocalizationOption("Eesti", "et");
+ yield return new LocalizationOption("فارسی", "fa");
+ yield return new LocalizationOption("Suomi", "fi");
+ yield return new LocalizationOption("Filipino", "fil");
+ yield return new LocalizationOption("Français", "fr");
+ yield return new LocalizationOption("Français (Canada)", "fr-CA");
+ yield return new LocalizationOption("Galego", "gl");
+ yield return new LocalizationOption("Schwiizerdütsch", "gsw");
+ yield return new LocalizationOption("עִבְרִית", "he");
+ yield return new LocalizationOption("हिन्दी", "hi");
+ yield return new LocalizationOption("Hrvatski", "hr");
+ yield return new LocalizationOption("Magyar", "hu");
+ yield return new LocalizationOption("Bahasa Indonesia", "id");
+ yield return new LocalizationOption("Íslenska", "is");
+ yield return new LocalizationOption("Italiano", "it");
+ yield return new LocalizationOption("日本語", "ja");
+ yield return new LocalizationOption("Qazaqşa", "kk");
+ yield return new LocalizationOption("한국어", "ko");
+ yield return new LocalizationOption("Lietuvių", "lt");
+ yield return new LocalizationOption("Latviešu", "lv");
+ yield return new LocalizationOption("Македонски", "mk");
+ yield return new LocalizationOption("മലയാളം", "ml");
+ yield return new LocalizationOption("मराठी", "mr");
+ yield return new LocalizationOption("Bahasa Melayu", "ms");
+ yield return new LocalizationOption("Norsk bokmål", "nb");
+ yield return new LocalizationOption("नेपाली", "ne");
+ yield return new LocalizationOption("Nederlands", "nl");
+ yield return new LocalizationOption("Norsk nynorsk", "nn");
+ yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa");
+ yield return new LocalizationOption("Polski", "pl");
+ yield return new LocalizationOption("Pirate", "pr");
+ yield return new LocalizationOption("Português", "pt");
+ yield return new LocalizationOption("Português (Brasil)", "pt-BR");
+ yield return new LocalizationOption("Português (Portugal)", "pt-PT");
+ yield return new LocalizationOption("Românește", "ro");
+ yield return new LocalizationOption("Русский", "ru");
+ yield return new LocalizationOption("Slovenčina", "sk");
+ yield return new LocalizationOption("Slovenščina", "sl-SI");
+ yield return new LocalizationOption("Shqip", "sq");
+ yield return new LocalizationOption("Српски", "sr");
+ yield return new LocalizationOption("Svenska", "sv");
+ yield return new LocalizationOption("தமிழ்", "ta");
+ yield return new LocalizationOption("తెలుగు", "te");
+ yield return new LocalizationOption("ภาษาไทย", "th");
+ yield return new LocalizationOption("Türkçe", "tr");
+ yield return new LocalizationOption("Українська", "uk");
+ yield return new LocalizationOption("اُردُو", "ur_PK");
yield return new LocalizationOption("Tiếng Việt", "vi");
+ yield return new LocalizationOption("汉语 (简化字)", "zh-CN");
+ yield return new LocalizationOption("漢語 (繁体字)", "zh-TW");
+ yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
}
}
}
diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json
index b08a3ae79..22ffc5e09 100644
--- a/Emby.Server.Implementations/Localization/countries.json
+++ b/Emby.Server.Implementations/Localization/countries.json
@@ -630,7 +630,7 @@
"TwoLetterISORegionName": "MD"
},
{
- "DisplayName": "Réunion",
+ "DisplayName": "Réunion",
"Name": "RE",
"ThreeLetterISORegionName": "REU",
"TwoLetterISORegionName": "RE"
diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt
index 488901822..66fba3330 100644
--- a/Emby.Server.Implementations/Localization/iso6392.txt
+++ b/Emby.Server.Implementations/Localization/iso6392.txt
@@ -349,7 +349,8 @@ pli||pi|Pali|pali
pol||pl|Polish|polonais
pon|||Pohnpeian|pohnpei
por||pt|Portuguese|portugais
-pob||pt-br|Portuguese (Brazil)|portugais
+pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
+pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
pra|||Prakrit languages|prâkrit, langues
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
pus||ps|Pushto; Pashto|pachto
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 8aaa1f7bb..6e1dc725d 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -23,7 +24,6 @@ namespace Emby.Server.Implementations.MediaEncoder
{
public class EncodingManager : IEncodingManager
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IFileSystem _fileSystem;
private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.MediaEncoder
var path = GetChapterImagePath(video, chapter.StartPositionTicks);
- if (!currentImages.Contains(path, StringComparer.OrdinalIgnoreCase))
+ if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
{
if (extractImages)
{
@@ -193,7 +193,7 @@ namespace Emby.Server.Implementations.MediaEncoder
private string GetChapterImagePath(Video video, long chapterPositionTicks)
{
- var filename = video.DateModified.Ticks.ToString(_usCulture) + "_" + chapterPositionTicks.ToString(_usCulture) + ".jpg";
+ var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
return Path.Combine(GetChapterImagesPath(video), filename);
}
@@ -220,7 +220,7 @@ namespace Emby.Server.Implementations.MediaEncoder
{
var deadImages = images
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparer.OrdinalIgnoreCase))
+ .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var image in deadImages)
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 137728616..6d0c8731e 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -11,6 +11,7 @@ namespace Emby.Server.Implementations.Net
{
public class SocketFactory : ISocketFactory
{
+ /// <inheritdoc />
public ISocket CreateUdpBroadcastSocket(int localPort)
{
if (localPort < 0)
@@ -35,11 +36,8 @@ namespace Emby.Server.Implementations.Net
}
}
- /// <summary>
- /// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port.
- /// </summary>
- /// <returns>An implementation of the <see cref="ISocket"/> interface used by RSSDP components to perform acceptSocket operations.</returns>
- public ISocket CreateSsdpUdpSocket(IPAddress localIpAddress, int localPort)
+ /// <inheritdoc />
+ public ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort)
{
if (localPort < 0)
{
@@ -53,8 +51,8 @@ namespace Emby.Server.Implementations.Net
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
- retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIpAddress));
- return new UdpSocket(retVal, localPort, localIpAddress);
+ retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp));
+ return new UdpSocket(retVal, localPort, localIp);
}
catch
{
@@ -64,13 +62,7 @@ namespace Emby.Server.Implementations.Net
}
}
- /// <summary>
- /// Creates a new UDP acceptSocket that is a member of the specified multicast IP address, and binds it to the specified local port.
- /// </summary>
- /// <param name="ipAddress">The multicast IP address to make the acceptSocket a member of.</param>
- /// <param name="multicastTimeToLive">The multicast time to live value for the acceptSocket.</param>
- /// <param name="localPort">The number of the local port to bind to.</param>
- /// <returns></returns>
+ /// <inheritdoc />
public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort)
{
if (ipAddress == null)
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index a8b18d292..0c451ccb6 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -16,11 +16,7 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable
{
- private Socket _socket;
private readonly int _localPort;
- private bool _disposed = false;
-
- public Socket Socket => _socket;
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{
@@ -32,6 +28,8 @@ namespace Emby.Server.Implementations.Net
SocketFlags = SocketFlags.None
};
+ private Socket _socket;
+ private bool _disposed = false;
private TaskCompletionSource<SocketReceiveResult> _currentReceiveTaskCompletionSource;
private TaskCompletionSource<int> _currentSendTaskCompletionSource;
@@ -64,6 +62,8 @@ namespace Emby.Server.Implementations.Net
InitReceiveSocketAsyncEventArgs();
}
+ public Socket Socket => _socket;
+
public IPAddress LocalIPAddress { get; }
private void InitReceiveSocketAsyncEventArgs()
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Net
return taskCompletion.Task;
}
- public Task SendToAsync(byte[] buffer, int offset, int size, IPEndPoint endPoint, CancellationToken cancellationToken)
+ public Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken)
{
ThrowIfDisposed();
@@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Net
}
};
- var result = BeginSendTo(buffer, offset, size, endPoint, new AsyncCallback(callback), null);
+ var result = BeginSendTo(buffer, offset, bytes, endPoint, new AsyncCallback(callback), null);
if (result.CompletedSynchronously)
{
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 9a1ca9946..9481e26f7 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -147,7 +147,7 @@ namespace Emby.Server.Implementations.Playlists
playlist.SetMediaType(options.MediaType);
- parentFolder.AddChild(playlist, CancellationToken.None);
+ parentFolder.AddChild(playlist);
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false);
@@ -260,7 +260,7 @@ namespace Emby.Server.Implementations.Playlists
public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
{
- if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
+ if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
throw new ArgumentException("No Playlist exists with the supplied Id");
}
@@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.Playlists
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
{
- if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
+ if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
throw new ArgumentException("No Playlist exists with the supplied Id");
}
@@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists
var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri);
string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
- if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase))
+ if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
{
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index 358606b0d..8ec9f6161 100644
--- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
@@ -17,6 +18,15 @@ namespace Emby.Server.Implementations.Playlists
Name = "Playlists";
}
+ [JsonIgnore]
+ public override bool IsHidden => true;
+
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ [JsonIgnore]
+ public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
+
public override bool IsVisible(User user)
{
return base.IsVisible(user) && GetChildren(user, true).Any();
@@ -27,15 +37,6 @@ namespace Emby.Server.Implementations.Playlists
return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>();
}
- [JsonIgnore]
- public override bool IsHidden => true;
-
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
-
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (query.User == null)
@@ -45,9 +46,14 @@ namespace Emby.Server.Implementations.Playlists
}
query.Recursive = true;
- query.IncludeItemTypes = new[] { "Playlist" };
+ query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
query.Parent = null;
return LibraryManager.GetItemsResult(query);
}
+
+ public override string GetClientTypeName()
+ {
+ return "ManualPlaylistsFolder";
+ }
}
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 8fd61f2bc..d70a15dbc 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -8,13 +8,14 @@ using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
@@ -38,14 +39,6 @@ namespace Emby.Server.Implementations.Plugins
private IHttpClientFactory? _httpClientFactory;
- private IHttpClientFactory HttpClientFactory
- {
- get
- {
- return _httpClientFactory ?? (_httpClientFactory = _appHost.Resolve<IHttpClientFactory>());
- }
- }
-
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
@@ -85,6 +78,14 @@ namespace Emby.Server.Implementations.Plugins
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
}
+ private IHttpClientFactory HttpClientFactory
+ {
+ get
+ {
+ return _httpClientFactory ??= _appHost.Resolve<IHttpClientFactory>();
+ }
+ }
+
/// <summary>
/// Gets the Plugins.
/// </summary>
@@ -125,7 +126,8 @@ namespace Emby.Server.Implementations.Plugins
{
assembly = Assembly.LoadFrom(file);
- assembly.GetExportedTypes();
+ // Load all required types to verify that the plugin will load
+ assembly.GetTypes();
}
catch (FileLoadException ex)
{
@@ -133,7 +135,7 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
- catch (TypeLoadException ex) // Undocumented exception
+ catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
{
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.NotSupported);
@@ -371,7 +373,7 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
- await using var fileStream = File.OpenWrite(imagePath);
+ await using var fileStream = AsyncFile.OpenWrite(imagePath);
try
{
diff --git a/Emby.Server.Implementations/Properties/AssemblyInfo.cs b/Emby.Server.Implementations/Properties/AssemblyInfo.cs
index cb7972173..41c396ac1 100644
--- a/Emby.Server.Implementations/Properties/AssemblyInfo.cs
+++ b/Emby.Server.Implementations/Properties/AssemblyInfo.cs
@@ -16,6 +16,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
+[assembly: InternalsVisibleTo("Emby.Server.Implementations.Fuzz")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
index 7cfd1fced..c81c26994 100644
--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -1,17 +1,15 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
+using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
-using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
using MediaBrowser.Model.QuickConnect;
using Microsoft.Extensions.Logging;
@@ -20,16 +18,24 @@ namespace Emby.Server.Implementations.QuickConnect
/// <summary>
/// Quick connect implementation.
/// </summary>
- public class QuickConnectManager : IQuickConnect, IDisposable
+ public class QuickConnectManager : IQuickConnect
{
- private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
- private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
+ /// <summary>
+ /// The length of user facing codes.
+ /// </summary>
+ private const int CodeLength = 6;
+
+ /// <summary>
+ /// The time (in minutes) that the quick connect token is valid.
+ /// </summary>
+ private const int Timeout = 10;
+
+ private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
+ private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
private readonly IServerConfigurationManager _config;
private readonly ILogger<QuickConnectManager> _logger;
- private readonly IAuthenticationRepository _authenticationRepository;
- private readonly IAuthorizationContext _authContext;
- private readonly IServerApplicationHost _appHost;
+ private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
@@ -37,87 +43,67 @@ namespace Emby.Server.Implementations.QuickConnect
/// </summary>
/// <param name="config">Configuration.</param>
/// <param name="logger">Logger.</param>
- /// <param name="appHost">Application host.</param>
- /// <param name="authContext">Authentication context.</param>
- /// <param name="authenticationRepository">Authentication repository.</param>
+ /// <param name="sessionManager">Session Manager.</param>
public QuickConnectManager(
IServerConfigurationManager config,
ILogger<QuickConnectManager> logger,
- IServerApplicationHost appHost,
- IAuthorizationContext authContext,
- IAuthenticationRepository authenticationRepository)
+ ISessionManager sessionManager)
{
_config = config;
_logger = logger;
- _appHost = appHost;
- _authContext = authContext;
- _authenticationRepository = authenticationRepository;
-
- ReloadConfiguration();
+ _sessionManager = sessionManager;
}
- /// <inheritdoc/>
- public int CodeLength { get; set; } = 6;
-
- /// <inheritdoc/>
- public string TokenName { get; set; } = "QuickConnect";
-
- /// <inheritdoc/>
- public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
-
- /// <inheritdoc/>
- public int Timeout { get; set; } = 5;
+ /// <inheritdoc />
+ public bool IsEnabled => _config.Configuration.QuickConnectAvailable;
- private DateTime DateActivated { get; set; }
-
- /// <inheritdoc/>
- public void AssertActive()
+ /// <summary>
+ /// Assert that quick connect is currently active and throws an exception if it is not.
+ /// </summary>
+ private void AssertActive()
{
- if (State != QuickConnectState.Active)
+ if (!IsEnabled)
{
- throw new ArgumentException("Quick connect is not active on this server");
+ throw new AuthenticationException("Quick connect is not active on this server");
}
}
/// <inheritdoc/>
- public void Activate()
+ public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
{
- DateActivated = DateTime.UtcNow;
- SetState(QuickConnectState.Active);
- }
-
- /// <inheritdoc/>
- public void SetState(QuickConnectState newState)
- {
- _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
-
- ExpireRequests(true);
-
- State = newState;
- _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
- _config.SaveConfiguration();
+ if (string.IsNullOrEmpty(authorizationInfo.DeviceId))
+ {
+ throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required");
+ }
- _logger.LogDebug("Configuration saved");
- }
+ if (string.IsNullOrEmpty(authorizationInfo.Device))
+ {
+ throw new ArgumentException(nameof(authorizationInfo.Device) + " is required");
+ }
- /// <inheritdoc/>
- public QuickConnectResult TryConnect()
- {
- ExpireRequests();
+ if (string.IsNullOrEmpty(authorizationInfo.Client))
+ {
+ throw new ArgumentException(nameof(authorizationInfo.Client) + " is required");
+ }
- if (State != QuickConnectState.Active)
+ if (string.IsNullOrEmpty(authorizationInfo.Version))
{
- _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
- throw new AuthenticationException("Quick connect is not active on this server");
+ throw new ArgumentException(nameof(authorizationInfo.Version) + "is required");
}
+ AssertActive();
+ ExpireRequests();
+
+ var secret = GenerateSecureRandom();
var code = GenerateCode();
- var result = new QuickConnectResult()
- {
- Secret = GenerateSecureRandom(),
- DateAdded = DateTime.UtcNow,
- Code = code
- };
+ var result = new QuickConnectResult(
+ secret,
+ code,
+ DateTime.UtcNow,
+ authorizationInfo.DeviceId,
+ authorizationInfo.Device,
+ authorizationInfo.Client,
+ authorizationInfo.Version);
_currentRequests[code] = result;
return result;
@@ -126,12 +112,12 @@ namespace Emby.Server.Implementations.QuickConnect
/// <inheritdoc/>
public QuickConnectResult CheckRequestStatus(string secret)
{
- ExpireRequests();
AssertActive();
+ ExpireRequests();
string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
- if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult? result))
{
throw new ResourceNotFoundException("Unable to find request with provided secret");
}
@@ -139,8 +125,11 @@ namespace Emby.Server.Implementations.QuickConnect
return result;
}
- /// <inheritdoc/>
- public string GenerateCode()
+ /// <summary>
+ /// Generates a short code to display to the user to uniquely identify this request.
+ /// </summary>
+ /// <returns>A short, unique alphanumeric string.</returns>
+ private string GenerateCode()
{
Span<byte> raw = stackalloc byte[4];
@@ -150,7 +139,7 @@ namespace Emby.Server.Implementations.QuickConnect
uint scale = uint.MaxValue;
while (scale == uint.MaxValue)
{
- _rng.GetBytes(raw);
+ RandomNumberGenerator.Fill(raw);
scale = BitConverter.ToUInt32(raw);
}
@@ -159,12 +148,12 @@ namespace Emby.Server.Implementations.QuickConnect
}
/// <inheritdoc/>
- public bool AuthorizeRequest(Guid userId, string code)
+ public async Task<bool> AuthorizeRequest(Guid userId, string code)
{
- ExpireRequests();
AssertActive();
+ ExpireRequests();
- if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult? result))
{
throw new ResourceNotFoundException("Unable to find request");
}
@@ -174,95 +163,62 @@ namespace Emby.Server.Implementations.QuickConnect
throw new InvalidOperationException("Request is already authorized");
}
- result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
-
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
- var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
- result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
+ result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
- _authenticationRepository.Create(new AuthenticationInfo
+ var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
{
- AppName = TokenName,
- AccessToken = result.Authentication,
- DateCreated = DateTime.UtcNow,
- DeviceId = _appHost.SystemId,
- DeviceName = _appHost.FriendlyName,
- AppVersion = _appHost.ApplicationVersionString,
- UserId = userId
- });
+ UserId = userId,
+ DeviceId = result.DeviceId,
+ DeviceName = result.DeviceName,
+ App = result.AppName,
+ AppVersion = result.AppVersion
+ }).ConfigureAwait(false);
+
+ _authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
+ result.Authenticated = true;
+ _currentRequests[code] = result;
- _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
+ _logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
return true;
}
/// <inheritdoc/>
- public int DeleteAllDevices(Guid user)
+ public AuthenticationResult GetAuthorizedRequest(string secret)
{
- var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
- {
- DeviceId = _appHost.SystemId,
- UserId = user
- });
-
- var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
+ AssertActive();
+ ExpireRequests();
- var removed = 0;
- foreach (var token in tokens)
+ if (!_authorizedSecrets.TryGetValue(secret, out var result))
{
- _authenticationRepository.Delete(token);
- _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
- removed++;
+ throw new ResourceNotFoundException("Unable to find request");
}
- return removed;
- }
-
- /// <summary>
- /// Dispose.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Dispose.
- /// </summary>
- /// <param name="disposing">Dispose unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- _rng?.Dispose();
- }
+ return result.AuthenticationResult;
}
private string GenerateSecureRandom(int length = 32)
{
Span<byte> bytes = stackalloc byte[length];
- _rng.GetBytes(bytes);
+ RandomNumberGenerator.Fill(bytes);
return Convert.ToHexString(bytes);
}
- /// <inheritdoc/>
- public void ExpireRequests(bool expireAll = false)
+ /// <summary>
+ /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.
+ /// </summary>
+ /// <param name="expireAll">If true, all requests will be expired.</param>
+ private void ExpireRequests(bool expireAll = false)
{
- // Check if quick connect should be deactivated
- if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
- {
- _logger.LogDebug("Quick connect time expired, deactivating");
- SetState(QuickConnectState.Available);
- expireAll = true;
- }
+ // All requests before this timestamp have expired
+ var minTime = DateTime.UtcNow.AddMinutes(-Timeout);
// Expire stale connection requests
foreach (var (_, currentRequest) in _currentRequests)
{
- var added = currentRequest.DateAdded ?? DateTime.UnixEpoch;
- if (expireAll || DateTime.UtcNow > added.AddMinutes(Timeout))
+ if (expireAll || currentRequest.DateAdded < minTime)
{
var code = currentRequest.Code;
_logger.LogDebug("Removing expired request {Code}", code);
@@ -273,11 +229,18 @@ namespace Emby.Server.Implementations.QuickConnect
}
}
}
- }
- private void ReloadConfiguration()
- {
- State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
+ foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
+ {
+ if (expireAll || timestamp < minTime)
+ {
+ _logger.LogDebug("Removing expired secret {Secret}", secret);
+ if (!_authorizedSecrets.TryRemove(secret, out _))
+ {
+ _logger.LogWarning("Secret {Secret} already expired", secret);
+ }
+ }
+ }
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index d7e320754..21a7f4f5f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -10,9 +10,9 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -24,6 +24,10 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class ScheduledTaskWorker : IScheduledTaskWorker
{
+ /// <summary>
+ /// The options for the json Serializer.
+ /// </summary>
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Gets or sets the application paths.
@@ -68,11 +72,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
private string _id;
/// <summary>
- /// The options for the json Serializer.
- /// </summary>
- private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
-
- /// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
/// </summary>
/// <param name="scheduledTask">The scheduled task.</param>
@@ -267,7 +266,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Gets the triggers that define when the task will run.
+ /// Gets or sets the triggers that define when the task will run.
/// </summary>
/// <value>The triggers.</value>
/// <exception cref="ArgumentNullException"><c>value</c> is <c>null</c>.</exception>
@@ -366,7 +365,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="options">Task options.</param>
/// <returns>Task.</returns>
- /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running</exception>
+ /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>
public async Task Execute(TaskOptions options)
{
var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
@@ -639,7 +638,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- _logger.LogInformation(Name + ": Cancelling");
+ _logger.LogInformation("{Name}: Cancelling", Name);
token.Cancel();
}
catch (Exception ex)
@@ -653,16 +652,16 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- _logger.LogInformation(Name + ": Waiting on Task");
+ _logger.LogInformation("{Name}: Waiting on Task", Name);
var exited = task.Wait(2000);
if (exited)
{
- _logger.LogInformation(Name + ": Task exited");
+ _logger.LogInformation("{Name}: Task exited", Name);
}
else
{
- _logger.LogInformation(Name + ": Timed out waiting for task to stop");
+ _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name);
}
}
catch (Exception ex)
@@ -675,7 +674,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- _logger.LogDebug(Name + ": Disposing CancellationToken");
+ _logger.LogDebug("{Name}: Disposing CancellationToken", Name);
token.Dispose();
}
catch (Exception ex)
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 4f0df75bf..0431858fc 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -19,16 +19,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
public class TaskManager : ITaskManager
{
- public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
-
- public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
-
- /// <summary>
- /// Gets the list of Scheduled Tasks.
- /// </summary>
- /// <value>The scheduled tasks.</value>
- public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
-
/// <summary>
/// The _task queue.
/// </summary>
@@ -53,10 +43,20 @@ namespace Emby.Server.Implementations.ScheduledTasks
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+
+ public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+
+ /// <summary>
+ /// Gets the list of Scheduled Tasks.
+ /// </summary>
+ /// <value>The scheduled tasks.</value>
+ public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
+
/// <summary>
/// Cancels if running and queue.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The task type.</typeparam>
/// <param name="options">Task options.</param>
public void CancelIfRunningAndQueue<T>(TaskOptions options)
where T : IScheduledTask
@@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Cancels if running.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The task type.</typeparam>
public void CancelIfRunning<T>()
where T : IScheduledTask
{
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Queues the scheduled task.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The task type.</typeparam>
/// <param name="options">Task options.</param>
public void QueueScheduledTask<T>(TaskOptions options)
where T : IScheduledTask
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index baeb86a22..8b185419f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -39,6 +40,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
+ /// <param name="libraryManager">The library manager.</param>.
+ /// <param name="itemRepo">The item repository.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <param name="encodingManager">The encoding manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="localization">The localization manager.</param>
public ChapterImagesTask(
ILibraryManager libraryManager,
IItemRepository itemRepo,
@@ -55,9 +62,19 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
- /// <summary>
- /// Creates the triggers that define when the task will run.
- /// </summary>
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public string Key => "RefreshChapterImages";
+
+ /// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -127,7 +144,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var key = video.Path + video.DateModified.Ticks;
- var extract = !previouslyFailedImages.Contains(key, StringComparer.OrdinalIgnoreCase);
+ var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase);
try
{
@@ -162,26 +179,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
}
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
- /// <inheritdoc />
- public string Key => "RefreshChapterImages";
-
- /// <inheritdoc />
- public bool IsHidden => false;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
index 50ba9bc89..79886cb52 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs
@@ -60,12 +60,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
- if (!retentionDays.HasValue || retentionDays <= 0)
+ if (!retentionDays.HasValue || retentionDays < 0)
{
throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
}
- var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1);
+ var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value);
return _activityManager.CleanAsync(startDate);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 692d1667d..0941902fc 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -29,6 +29,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
/// </summary>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
public DeleteCacheFileTask(
IApplicationPaths appPaths,
ILogger<DeleteCacheFileTask> logger,
@@ -157,11 +161,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
catch (UnauthorizedAccessException ex)
{
- _logger.LogError(ex, "Error deleting directory {path}", directory);
+ _logger.LogError(ex, "Error deleting directory {Path}", directory);
}
catch (IOException ex)
{
- _logger.LogError(ex, "Error deleting directory {path}", directory);
+ _logger.LogError(ex, "Error deleting directory {Path}", directory);
}
}
}
@@ -175,11 +179,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
catch (UnauthorizedAccessException ex)
{
- _logger.LogError(ex, "Error deleting file {path}", path);
+ _logger.LogError(ex, "Error deleting file {Path}", path);
}
catch (IOException ex)
{
- _logger.LogError(ex, "Error deleting file {path}", path);
+ _logger.LogError(ex, "Error deleting file {Path}", path);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index b13fc7fc6..099d781cd 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -141,11 +141,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
catch (UnauthorizedAccessException ex)
{
- _logger.LogError(ex, "Error deleting directory {path}", directory);
+ _logger.LogError(ex, "Error deleting directory {Path}", directory);
}
catch (IOException ex)
{
- _logger.LogError(ex, "Error deleting directory {path}", directory);
+ _logger.LogError(ex, "Error deleting directory {Path}", directory);
}
}
}
@@ -159,11 +159,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
catch (UnauthorizedAccessException ex)
{
- _logger.LogError(ex, "Error deleting file {path}", path);
+ _logger.LogError(ex, "Error deleting file {Path}", path);
}
catch (IOException ex)
{
- _logger.LogError(ex, "Error deleting file {path}", path);
+ _logger.LogError(ex, "Error deleting file {Path}", path);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 1ad1d0f50..35a4aeef6 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -22,6 +22,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <summary>
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="localization">The localization manager.</param>
+ /// <param name="provider">The jellyfin DB context provider.</param>
public OptimizeDatabaseTask(
ILogger<OptimizeDatabaseTask> logger,
ILocalizationManager localization,
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index 57d294a40..53c692a46 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -32,9 +32,24 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
+ public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
+
+ public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
+
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ public string Key => "RefreshPeople";
+
+ public bool IsHidden => false;
+
+ public bool IsEnabled => true;
+
+ public bool IsLogged => true;
+
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
+ /// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -57,19 +72,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
return _libraryManager.ValidatePeople(cancellationToken, progress);
}
-
- public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
-
- public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
-
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
- public string Key => "RefreshPeople";
-
- public bool IsHidden => false;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index 51b620404..2184b3d03 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -33,6 +33,18 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+
+ /// <inheritdoc />
+ public string Key => "RefreshLibrary";
+
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
@@ -60,26 +72,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
}
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
-
- /// <inheritdoc />
- public string Key => "RefreshLibrary";
-
- /// <inheritdoc />
- public bool IsHidden => false;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
deleted file mode 100644
index e8eac315b..000000000
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ /dev/null
@@ -1,408 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Security
-{
- public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
- {
- public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config)
- : base(logger)
- {
- DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
- }
-
- public void Initialize()
- {
- string[] queries =
- {
- "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)",
- "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)",
- "drop index if exists idx_AccessTokens",
- "drop index if exists Tokens1",
- "drop index if exists Tokens2",
-
- "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)",
- "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)",
- "create index if not exists Devices1 on Devices (Id)"
- };
-
- using (var connection = GetConnection())
- {
- var tableNewlyCreated = !TableExists(connection, "Tokens");
-
- connection.RunQueries(queries);
-
- TryMigrate(connection, tableNewlyCreated);
- }
- }
-
- private void TryMigrate(ManagedConnection connection, bool tableNewlyCreated)
- {
- try
- {
- if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
- {
- connection.RunInTransaction(
- db =>
- {
- var existingColumnNames = GetColumnNames(db, "AccessTokens");
-
- AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames);
- AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames);
- AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames);
- }, TransactionMode);
-
- connection.RunQueries(new[]
- {
- "update accesstokens set DateLastActivity=DateCreated where DateLastActivity is null",
- "update accesstokens set DeviceName='Unknown' where DeviceName is null",
- "update accesstokens set AppName='Unknown' where AppName is null",
- "update accesstokens set AppVersion='1' where AppVersion is null",
- "INSERT INTO Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) SELECT AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity FROM AccessTokens where deviceid not null and devicename not null and appname not null and isactive=1"
- });
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error migrating authentication database");
- }
- }
-
- public void Create(AuthenticationInfo info)
- {
- if (info == null)
- {
- throw new ArgumentNullException(nameof(info));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
- {
- statement.TryBind("@AccessToken", info.AccessToken);
-
- statement.TryBind("@DeviceId", info.DeviceId);
- statement.TryBind("@AppName", info.AppName);
- statement.TryBind("@AppVersion", info.AppVersion);
- statement.TryBind("@DeviceName", info.DeviceName);
- statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
- statement.TryBind("@UserName", info.UserName);
- statement.TryBind("@IsActive", true);
- statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
- statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
-
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- public void Update(AuthenticationInfo info)
- {
- if (info == null)
- {
- throw new ArgumentNullException(nameof(info));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
- {
- statement.TryBind("@Id", info.Id);
-
- statement.TryBind("@AccessToken", info.AccessToken);
-
- statement.TryBind("@DeviceId", info.DeviceId);
- statement.TryBind("@AppName", info.AppName);
- statement.TryBind("@AppVersion", info.AppVersion);
- statement.TryBind("@DeviceName", info.DeviceName);
- statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
- statement.TryBind("@UserName", info.UserName);
- statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
- statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
-
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- public void Delete(AuthenticationInfo info)
- {
- if (info == null)
- {
- throw new ArgumentNullException(nameof(info));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
- {
- statement.TryBind("@Id", info.Id);
-
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- private const string BaseSelectText = "select Tokens.Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, DateCreated, DateLastActivity, Devices.CustomName from Tokens left join Devices on Tokens.DeviceId=Devices.Id";
-
- private static void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement)
- {
- if (!string.IsNullOrEmpty(query.AccessToken))
- {
- statement.TryBind("@AccessToken", query.AccessToken);
- }
-
- if (!query.UserId.Equals(Guid.Empty))
- {
- statement.TryBind("@UserId", query.UserId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- if (!string.IsNullOrEmpty(query.DeviceId))
- {
- statement.TryBind("@DeviceId", query.DeviceId);
- }
- }
-
- public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query)
- {
- if (query == null)
- {
- throw new ArgumentNullException(nameof(query));
- }
-
- var commandText = BaseSelectText;
-
- var whereClauses = new List<string>();
-
- if (!string.IsNullOrEmpty(query.AccessToken))
- {
- whereClauses.Add("AccessToken=@AccessToken");
- }
-
- if (!string.IsNullOrEmpty(query.DeviceId))
- {
- whereClauses.Add("DeviceId=@DeviceId");
- }
-
- if (!query.UserId.Equals(Guid.Empty))
- {
- whereClauses.Add("UserId=@UserId");
- }
-
- if (query.HasUser.HasValue)
- {
- if (query.HasUser.Value)
- {
- whereClauses.Add("UserId not null");
- }
- else
- {
- whereClauses.Add("UserId is null");
- }
- }
-
- var whereTextWithoutPaging = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- commandText += whereTextWithoutPaging;
-
- commandText += " ORDER BY DateLastActivity desc";
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
- }
-
- if (offset > 0)
- {
- commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
- }
- }
-
- var statementTexts = new[]
- {
- commandText,
- "select count (Id) from Tokens" + whereTextWithoutPaging
- };
-
- var list = new List<AuthenticationInfo>();
- var result = new QueryResult<AuthenticationInfo>();
- using (var connection = GetConnection(true))
- {
- connection.RunInTransaction(
- db =>
- {
- var statements = PrepareAll(db, statementTexts);
-
- using (var statement = statements[0])
- {
- BindAuthenticationQueryParams(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(Get(row));
- }
-
- using (var totalCountStatement = statements[1])
- {
- BindAuthenticationQueryParams(query, totalCountStatement);
-
- result.TotalRecordCount = totalCountStatement.ExecuteQuery()
- .SelectScalarInt()
- .First();
- }
- }
- },
- ReadTransactionMode);
- }
-
- result.Items = list;
- return result;
- }
-
- private static AuthenticationInfo Get(IReadOnlyList<ResultSetValue> reader)
- {
- var info = new AuthenticationInfo
- {
- Id = reader[0].ToInt64(),
- AccessToken = reader[1].ToString()
- };
-
- if (reader.TryGetString(2, out var deviceId))
- {
- info.DeviceId = deviceId;
- }
-
- if (reader.TryGetString(3, out var appName))
- {
- info.AppName = appName;
- }
-
- if (reader.TryGetString(4, out var appVersion))
- {
- info.AppVersion = appVersion;
- }
-
- if (reader.TryGetString(6, out var userId))
- {
- info.UserId = new Guid(userId);
- }
-
- if (reader.TryGetString(7, out var userName))
- {
- info.UserName = userName;
- }
-
- info.DateCreated = reader[8].ReadDateTime();
-
- if (reader.TryReadDateTime(9, out var dateLastActivity))
- {
- info.DateLastActivity = dateLastActivity;
- }
- else
- {
- info.DateLastActivity = info.DateCreated;
- }
-
- if (reader.TryGetString(10, out var customName))
- {
- info.DeviceName = customName;
- }
- else if (reader.TryGetString(5, out var deviceName))
- {
- info.DeviceName = deviceName;
- }
-
- return info;
- }
-
- public DeviceOptions GetDeviceOptions(string deviceId)
- {
- using (var connection = GetConnection(true))
- {
- return connection.RunInTransaction(
- db =>
- {
- using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
- {
- statement.TryBind("@DeviceId", deviceId);
-
- var result = new DeviceOptions();
-
- foreach (var row in statement.ExecuteQuery())
- {
- if (row.TryGetString(0, out var customName))
- {
- result.CustomName = customName;
- }
- }
-
- return result;
- }
- }, ReadTransactionMode);
- }
- }
-
- public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
- {
- if (options == null)
- {
- throw new ArgumentNullException(nameof(options));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
- {
- statement.TryBind("@Id", deviceId);
-
- if (string.IsNullOrWhiteSpace(options.CustomName))
- {
- statement.TryBindNull("@CustomName");
- }
- else
- {
- statement.TryBind("@CustomName", options.CustomName);
- }
-
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 5ff73de81..1bac2600c 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Serialization
private static XmlSerializer GetSerializer(Type type)
=> _serializers.GetOrAdd(
type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
- (_, t) => new XmlSerializer(t),
+ static (_, t) => new XmlSerializer(t),
type);
/// <summary>
@@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="file">The file.</param>
public void SerializeToFile(object obj, string file)
{
- using (var stream = new FileStream(file, FileMode.Create))
+ using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write))
{
SerializeToStream(obj, stream);
}
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index 6cf9a8f71..369a2b0d8 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -12,6 +12,11 @@ namespace Emby.Server.Implementations
/// <summary>
/// Initializes a new instance of the <see cref="ServerApplicationPaths" /> class.
/// </summary>
+ /// <param name="programDataPath">The path for Jellyfin's data.</param>
+ /// <param name="logDirectoryPath">The path for Jellyfin's logging directory.</param>
+ /// <param name="configurationDirectoryPath">The path for Jellyfin's configuration directory.</param>
+ /// <param name="cacheDirectoryPath">The path for Jellyfin's cache directory.</param>
+ /// <param name="webDirectoryPath">The path for Jellyfin's web UI.</param>
public ServerApplicationPaths(
string programDataPath,
string logDirectoryPath,
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 62df354fd..d10a24bbc 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -10,8 +10,11 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -24,9 +27,7 @@ using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library;
@@ -54,7 +55,6 @@ namespace Emby.Server.Implementations.Session
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
- private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
/// <summary>
@@ -77,7 +77,6 @@ namespace Emby.Server.Implementations.Session
IDtoService dtoService,
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
- IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
{
@@ -90,7 +89,6 @@ namespace Emby.Server.Implementations.Session
_dtoService = dtoService;
_imageProcessor = imageProcessor;
_appHost = appHost;
- _authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
@@ -237,12 +235,12 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public void UpdateDeviceName(string sessionId, string deviceName)
+ public void UpdateDeviceName(string sessionId, string reportedDeviceName)
{
var session = GetSession(sessionId);
if (session != null)
{
- session.DeviceName = deviceName;
+ session.DeviceName = reportedDeviceName;
}
}
@@ -256,7 +254,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- public SessionInfo LogSessionActivity(
+ public async Task<SessionInfo> LogSessionActivity(
string appName,
string appVersion,
string deviceId,
@@ -282,7 +280,7 @@ namespace Emby.Server.Implementations.Session
}
var activityDate = DateTime.UtcNow;
- var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
+ var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate;
@@ -295,7 +293,7 @@ namespace Emby.Server.Implementations.Session
try
{
user.LastActivityDate = activityDate;
- _userManager.UpdateUser(user);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException e)
{
@@ -318,14 +316,14 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public void OnSessionControllerConnected(SessionInfo info)
+ public void OnSessionControllerConnected(SessionInfo session)
{
EventHelper.QueueEventIfNotNull(
SessionControllerConnected,
this,
new SessionEventArgs
{
- SessionInfo = info
+ SessionInfo = session
},
_logger);
}
@@ -460,7 +458,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- private SessionInfo GetSessionInfo(
+ private async Task<SessionInfo> GetSessionInfo(
string appName,
string appVersion,
string deviceId,
@@ -479,9 +477,11 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
- var sessionInfo = _activeConnections.GetOrAdd(
- key,
- k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user));
+ if (!_activeConnections.TryGetValue(key, out var sessionInfo))
+ {
+ _activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ sessionInfo = _activeConnections[key];
+ }
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = user?.Username;
@@ -504,7 +504,7 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
- private SessionInfo CreateSession(
+ private async Task<SessionInfo> CreateSession(
string key,
string appName,
string appVersion,
@@ -534,7 +534,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
- var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
+ var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
if (string.IsNullOrEmpty(deviceOptions.CustomName))
{
sessionInfo.DeviceName = deviceName;
@@ -741,6 +741,8 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Used to report playback progress for an item.
/// </summary>
+ /// <param name="info">The playback progress info.</param>
+ /// <param name="isAutomated">Whether this is an automated update.</param>
/// <returns>Task.</returns>
public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
{
@@ -1199,16 +1201,18 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public async Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
+ var session = GetSession(sessionId);
await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
- public async Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken)
{
CheckDisposed();
+ var session = GetSession(sessionId);
await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
@@ -1288,7 +1292,7 @@ namespace Emby.Server.Implementations.Session
{
["ItemId"] = command.ItemId,
["ItemName"] = command.ItemName,
- ["ItemType"] = command.ItemType
+ ["ItemType"] = command.ItemType.ToString()
}
};
@@ -1432,38 +1436,20 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Authenticates the new session.
/// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{SessionInfo}.</returns>
+ /// <param name="request">The authenticationrequest.</param>
+ /// <returns>The authentication result.</returns>
public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
{
return AuthenticateNewSessionInternal(request, true);
}
- public Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request)
- {
- return AuthenticateNewSessionInternal(request, false);
- }
-
- public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
+ /// <summary>
+ /// Directly authenticates the session without enforcing password.
+ /// </summary>
+ /// <param name="request">The authentication request.</param>
+ /// <returns>The authentication result.</returns>
+ public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
{
- var result = _authRepo.Get(new AuthenticationInfoQuery()
- {
- AccessToken = token,
- DeviceId = _appHost.SystemId,
- Limit = 1
- });
-
- if (result.TotalRecordCount == 0)
- {
- throw new SecurityException("Unknown quick connect token");
- }
-
- var info = result.Items[0];
- request.UserId = info.UserId;
-
- // There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
- _authRepo.Delete(info);
-
return AuthenticateNewSessionInternal(request, false);
}
@@ -1509,15 +1495,15 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is at their maximum number of sessions.");
}
- var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
+ var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
- var session = LogSessionActivity(
+ var session = await LogSessionActivity(
request.App,
request.AppVersion,
request.DeviceId,
request.DeviceName,
request.RemoteEndPoint,
- user);
+ user).ConfigureAwait(false);
var returnResult = new AuthenticationResult
{
@@ -1532,36 +1518,33 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
- private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
+ private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
- var existing = _authRepo.Get(
- new AuthenticationInfoQuery
+ var existing = (await _deviceManager.GetDevices(
+ new DeviceQuery
{
DeviceId = deviceId,
UserId = user.Id,
Limit = 1
- }).Items.FirstOrDefault();
+ }).ConfigureAwait(false)).Items.FirstOrDefault();
- if (!string.IsNullOrEmpty(deviceId))
- {
- var allExistingForDevice = _authRepo.Get(
- new AuthenticationInfoQuery
- {
- DeviceId = deviceId
- }).Items;
+ var allExistingForDevice = (await _deviceManager.GetDevices(
+ new DeviceQuery
+ {
+ DeviceId = deviceId
+ }).ConfigureAwait(false)).Items;
- foreach (var auth in allExistingForDevice)
+ foreach (var auth in allExistingForDevice)
+ {
+ if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
{
- if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
+ try
{
- try
- {
- Logout(auth);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error while logging out.");
- }
+ await Logout(auth).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while logging out.");
}
}
}
@@ -1572,29 +1555,14 @@ namespace Emby.Server.Implementations.Session
return existing.AccessToken;
}
- var now = DateTime.UtcNow;
-
- var newToken = new AuthenticationInfo
- {
- AppName = app,
- AppVersion = appVersion,
- DateCreated = now,
- DateLastActivity = now,
- DeviceId = deviceId,
- DeviceName = deviceName,
- UserId = user.Id,
- AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- UserName = user.Username
- };
-
_logger.LogInformation("Creating new access token for user {0}", user.Id);
- _authRepo.Create(newToken);
+ var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
- return newToken.AccessToken;
+ return device.AccessToken;
}
/// <inheritdoc />
- public void Logout(string accessToken)
+ public async Task Logout(string accessToken)
{
CheckDisposed();
@@ -1603,30 +1571,30 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(accessToken));
}
- var existing = _authRepo.Get(
- new AuthenticationInfoQuery
+ var existing = (await _deviceManager.GetDevices(
+ new DeviceQuery
{
Limit = 1,
AccessToken = accessToken
- }).Items;
+ }).ConfigureAwait(false)).Items;
if (existing.Count > 0)
{
- Logout(existing[0]);
+ await Logout(existing[0]).ConfigureAwait(false);
}
}
/// <inheritdoc />
- public void Logout(AuthenticationInfo existing)
+ public async Task Logout(Device device)
{
CheckDisposed();
- _logger.LogInformation("Logging out access token {0}", existing.AccessToken);
+ _logger.LogInformation("Logging out access token {0}", device.AccessToken);
- _authRepo.Delete(existing);
+ await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
var sessions = Sessions
- .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var session in sessions)
@@ -1637,36 +1605,30 @@ namespace Emby.Server.Implementations.Session
}
catch (Exception ex)
{
- _logger.LogError("Error reporting session ended", ex);
+ _logger.LogError(ex, "Error reporting session ended");
}
}
}
/// <inheritdoc />
- public void RevokeUserTokens(Guid userId, string currentAccessToken)
+ public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
{
CheckDisposed();
- var existing = _authRepo.Get(new AuthenticationInfoQuery
+ var existing = await _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
- });
+ }).ConfigureAwait(false);
foreach (var info in existing.Items)
{
if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
{
- Logout(info);
+ await Logout(info).ConfigureAwait(false);
}
}
}
- /// <inheritdoc />
- public void RevokeToken(string token)
- {
- Logout(token);
- }
-
/// <summary>
/// Reports the capabilities.
/// </summary>
@@ -1786,18 +1748,9 @@ namespace Emby.Server.Implementations.Session
}
var item = _libraryManager.GetItemById(new Guid(itemId));
-
- var info = GetItemInfo(item, null);
-
- ReportNowViewingItem(sessionId, info);
- }
-
- /// <inheritdoc />
- public void ReportNowViewingItem(string sessionId, BaseItemDto item)
- {
var session = GetSession(sessionId);
- session.NowViewingItem = item;
+ session.NowViewingItem = GetItemInfo(item, null);
}
/// <inheritdoc />
@@ -1827,7 +1780,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
+ public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion)
{
if (info == null)
{
@@ -1860,20 +1813,20 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
+ public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
- var items = _authRepo.Get(new AuthenticationInfoQuery
+ var items = (await _deviceManager.GetDevices(new DeviceQuery
{
AccessToken = token,
Limit = 1
- }).Items;
+ }).ConfigureAwait(false)).Items;
if (items.Count == 0)
{
return null;
}
- return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null);
+ return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index e9e3ca7f4..2a14a8c7b 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection)
{
- var session = GetSession(connection.QueryString, connection.RemoteEndPoint.ToString());
+ var session = await GetSession(connection.QueryString, connection.RemoteEndPoint.ToString()).ConfigureAwait(false);
if (session != null)
{
EnsureController(session, connection);
@@ -111,7 +111,7 @@ namespace Emby.Server.Implementations.Session
}
}
- private SessionInfo GetSession(IQueryCollection queryString, string remoteEndpoint)
+ private Task<SessionInfo> GetSession(IQueryCollection queryString, string remoteEndpoint)
{
if (queryString == null)
{
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 2b0ab536f..db8b68949 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class AiredEpisodeOrderComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.AiredEpisodeOrder;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -28,16 +34,6 @@ namespace Emby.Server.Implementations.Sorting
throw new ArgumentNullException(nameof(y));
}
- if (x.PremiereDate.HasValue && y.PremiereDate.HasValue)
- {
- var val = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
-
- if (val != 0)
- {
- // return val;
- }
- }
-
var episode1 = x as Episode;
var episode2 = y as Episode;
@@ -156,14 +152,14 @@ namespace Emby.Server.Implementations.Sorting
{
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
+ var comparisonResult = xValue.CompareTo(yValue);
+ // If equal, compare premiere dates
+ if (comparisonResult == 0 && x.PremiereDate.HasValue && y.PremiereDate.HasValue)
+ {
+ comparisonResult = DateTime.Compare(x.PremiereDate.Value, y.PremiereDate.Value);
+ }
- return xValue.CompareTo(yValue);
+ return comparisonResult;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.AiredEpisodeOrder;
}
}
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 42e644970..67a9fbd3b 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -13,6 +13,12 @@ namespace Emby.Server.Implementations.Sorting
public class AlbumArtistComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.AlbumArtist;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -20,7 +26,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>System.Int32.</returns>
public int Compare(BaseItem? x, BaseItem? y)
{
- return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
@@ -34,11 +40,5 @@ namespace Emby.Server.Implementations.Sorting
return audio?.AlbumArtists.FirstOrDefault();
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.AlbumArtist;
}
}
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 1db3f5e9c..4d09dda84 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -12,6 +12,12 @@ namespace Emby.Server.Implementations.Sorting
public class AlbumComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Album;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -19,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>System.Int32.</returns>
public int Compare(BaseItem? x, BaseItem? y)
{
- return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
@@ -29,15 +35,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>System.String.</returns>
private static string? GetValue(BaseItem? x)
{
- var audio = x as Audio;
-
- return audio == null ? string.Empty : audio.Album;
+ return x is Audio audio ? audio.Album : string.Empty;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Album;
}
}
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index 98bee3fd9..a8bb55e2b 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// <inheritdoc />
public int Compare(BaseItem? x, BaseItem? y)
{
- return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
}
/// <summary>
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>System.String.</returns>
private static string? GetValue(BaseItem? x)
{
- if (!(x is Audio audio))
+ if (x is not Audio audio)
{
return string.Empty;
}
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index d20dedc2d..ba1835e4f 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -10,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
public class CriticRatingComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.CriticRating;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -24,11 +30,5 @@ namespace Emby.Server.Implementations.Sorting
{
return x?.CriticRating ?? 0;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.CriticRating;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index d3f10f78c..8b460166c 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class DateCreatedComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.DateCreated;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -30,11 +36,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.Compare(x.DateCreated, y.DateCreated);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.DateCreated;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 08a44319f..ec818253b 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -33,6 +33,12 @@ namespace Emby.Server.Implementations.Sorting
public IUserDataManager UserDataRepository { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.DatePlayed;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -59,11 +65,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.MinValue;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.DatePlayed;
}
}
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 4de81a69e..c2875eeb9 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class NameComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Name;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -28,13 +34,7 @@ namespace Emby.Server.Implementations.Sorting
throw new ArgumentNullException(nameof(y));
}
- return string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase);
+ return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Name;
}
}
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 04e4865cb..45c9044c5 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -20,6 +20,24 @@ namespace Emby.Server.Implementations.Sorting
public User User { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.PlayCount;
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -41,23 +59,5 @@ namespace Emby.Server.Implementations.Sorting
return userdata == null ? 0 : userdata.PlayCount;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.PlayCount;
-
- /// <summary>
- /// Gets or sets the user data repository.
- /// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
}
}
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index c98f97bf1..b217556ef 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class PremiereDateComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.PremiereDate;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -52,11 +58,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.MinValue;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.PremiereDate;
}
}
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index df9f9957d..d2022df7a 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -10,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
public class ProductionYearComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.ProductionYear;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -44,11 +50,5 @@ namespace Emby.Server.Implementations.Sorting
return 0;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.ProductionYear;
}
}
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index af3bc2750..bf0168222 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -11,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class RandomComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Random;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -20,11 +26,5 @@ namespace Emby.Server.Implementations.Sorting
{
return Guid.NewGuid().CompareTo(Guid.NewGuid());
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Random;
}
}
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 129315303..e32e5552e 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -13,6 +13,12 @@ namespace Emby.Server.Implementations.Sorting
public class RuntimeComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Runtime;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -32,11 +38,5 @@ namespace Emby.Server.Implementations.Sorting
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Runtime;
}
}
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 4123a59f8..0bd9600b9 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y)
{
- return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
+ return string.Compare(GetValue(x), GetValue(y), StringComparison.OrdinalIgnoreCase);
}
private static string GetValue(BaseItem item)
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 8d30716d3..79be9a89a 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -13,6 +13,12 @@ namespace Emby.Server.Implementations.Sorting
public class SortNameComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.SortName;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -30,13 +36,7 @@ namespace Emby.Server.Implementations.Sorting
throw new ArgumentNullException(nameof(y));
}
- return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
+ return string.Compare(x.SortName, y.SortName, StringComparison.OrdinalIgnoreCase);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.SortName;
}
}
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 01445c525..4d89cfa8b 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,6 +14,12 @@ namespace Emby.Server.Implementations.Sorting
public class StudioComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.Studio;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -30,13 +37,7 @@ namespace Emby.Server.Implementations.Sorting
throw new ArgumentNullException(nameof(y));
}
- return AlphanumComparator.CompareValues(x.Studios.FirstOrDefault() ?? string.Empty, y.Studios.FirstOrDefault() ?? string.Empty);
+ return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault());
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.Studio;
}
}
diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs
index 12efff261..75cf890e5 100644
--- a/Emby.Server.Implementations/SyncPlay/Group.cs
+++ b/Emby.Server.Implementations/SyncPlay/Group.cs
@@ -164,26 +164,26 @@ namespace Emby.Server.Implementations.SyncPlay
/// <summary>
/// Filters sessions of this group.
/// </summary>
- /// <param name="from">The current session.</param>
+ /// <param name="fromId">The current session identifier.</param>
/// <param name="type">The filtering type.</param>
/// <returns>The list of sessions matching the filter.</returns>
- private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, SyncPlayBroadcastType type)
+ private IEnumerable<string> FilterSessions(string fromId, SyncPlayBroadcastType type)
{
return type switch
{
- SyncPlayBroadcastType.CurrentSession => new SessionInfo[] { from },
+ SyncPlayBroadcastType.CurrentSession => new string[] { fromId },
SyncPlayBroadcastType.AllGroup => _participants
.Values
- .Select(session => session.Session),
+ .Select(member => member.SessionId),
SyncPlayBroadcastType.AllExceptCurrentSession => _participants
.Values
- .Select(session => session.Session)
- .Where(session => !session.Id.Equals(from.Id, StringComparison.OrdinalIgnoreCase)),
+ .Select(member => member.SessionId)
+ .Where(sessionId => !sessionId.Equals(fromId, StringComparison.OrdinalIgnoreCase)),
SyncPlayBroadcastType.AllReady => _participants
.Values
- .Where(session => !session.IsBuffering)
- .Select(session => session.Session),
- _ => Enumerable.Empty<SessionInfo>()
+ .Where(member => !member.IsBuffering)
+ .Select(member => member.SessionId),
+ _ => Enumerable.Empty<string>()
};
}
@@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.SyncPlay
// Get list of users.
var users = _participants
.Values
- .Select(participant => _userManager.GetUserById(participant.Session.UserId));
+ .Select(participant => _userManager.GetUserById(participant.UserId));
// Find problematic users.
var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue));
@@ -353,7 +353,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <returns>The group info for the clients.</returns>
public GroupInfoDto GetInfo()
{
- var participants = _participants.Values.Select(session => session.Session.UserName).Distinct().ToList();
+ var participants = _participants.Values.Select(session => session.UserName).Distinct().ToList();
return new GroupInfoDto(GroupId, GroupName, _state.Type, participants, DateTime.UtcNow);
}
@@ -389,9 +389,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- foreach (var session in FilterSessions(from, type))
+ foreach (var sessionId in FilterSessions(from.Id, type))
{
- yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayGroupUpdate(sessionId, message, cancellationToken);
}
}
@@ -403,9 +403,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- foreach (var session in FilterSessions(from, type))
+ foreach (var sessionId in FilterSessions(from.Id, type))
{
- yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayCommand(sessionId, message, cancellationToken);
}
}
@@ -537,6 +537,16 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
+ public void ClearPlayQueue(bool clearPlayingItem)
+ {
+ PlayQueue.ClearPlaylist(clearPlayingItem);
+ if (clearPlayingItem)
+ {
+ RestartCurrentItem();
+ }
+ }
+
+ /// <inheritdoc />
public bool RemoveFromPlayQueue(IReadOnlyList<Guid> playlistItemIds)
{
var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds);
@@ -649,8 +659,9 @@ namespace Emby.Server.Implementations.SyncPlay
public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason)
{
var startPositionTicks = PositionTicks;
+ var isPlaying = _state.Type.Equals(GroupStateType.Playing);
- if (_state.Type.Equals(GroupStateType.Playing))
+ if (isPlaying)
{
var currentTime = DateTime.UtcNow;
var elapsedTime = currentTime - LastActivity;
@@ -669,6 +680,7 @@ namespace Emby.Server.Implementations.SyncPlay
PlayQueue.GetPlaylist(),
PlayQueue.PlayingItemIndex,
startPositionTicks,
+ isPlaying,
PlayQueue.ShuffleMode,
PlayQueue.RepeatMode);
}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 993456196..2ebeea717 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -160,7 +160,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId);
var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -172,7 +172,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue.", session.Id, group.GroupId.ToString());
var error = new GroupUpdate<string>(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -249,7 +249,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
}
@@ -329,7 +329,7 @@ namespace Emby.Server.Implementations.SyncPlay
_logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id);
var error = new GroupUpdate<string>(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty);
- _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index af453d148..a87831294 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -33,9 +33,9 @@ namespace Emby.Server.Implementations.TV
_configurationManager = configurationManager;
}
- public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
+ public QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options)
{
- var user = _userManager.GetUserById(request.UserId);
+ var user = _userManager.GetUserById(query.UserId);
if (user == null)
{
@@ -43,9 +43,9 @@ namespace Emby.Server.Implementations.TV
}
string presentationUniqueKey = null;
- if (!string.IsNullOrEmpty(request.SeriesId))
+ if (!string.IsNullOrEmpty(query.SeriesId))
{
- if (_libraryManager.GetItemById(request.SeriesId) is Series series)
+ if (_libraryManager.GetItemById(query.SeriesId) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
}
@@ -53,14 +53,14 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request);
+ return GetResult(GetNextUpEpisodes(query, user, new[] { presentationUniqueKey }, options), query);
}
BaseItem[] parents;
- if (request.ParentId.HasValue)
+ if (query.ParentId.HasValue)
{
- var parent = _libraryManager.GetItemById(request.ParentId.Value);
+ var parent = _libraryManager.GetItemById(query.ParentId.Value);
if (parent != null)
{
@@ -79,10 +79,10 @@ namespace Emby.Server.Implementations.TV
.ToArray();
}
- return GetNextUp(request, parents, dtoOptions);
+ return GetNextUp(query, parents, options);
}
- public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions dtoOptions)
+ public QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options)
{
var user = _userManager.GetUserById(request.UserId);
@@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.TV
if (!string.IsNullOrEmpty(presentationUniqueKey))
{
- return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request);
+ return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request);
}
if (limit.HasValue)
@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.TV
.GetItemList(
new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
SeriesPresentationUniqueKey = presentationUniqueKey,
Limit = limit,
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.TV
.Select(GetUniqueSeriesKey);
// Avoid implicitly captured closure
- var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
+ var episodes = GetNextUpEpisodes(request, user, items, options);
return GetResult(episodes, request);
}
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
@@ -209,7 +209,7 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) },
Limit = 1,
IsPlayed = false,
@@ -226,7 +226,7 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
IsPlayed = false,
IsVirtualItem = false,
DtoOptions = dtoOptions
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 8179e26c5..33e4e5651 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -29,10 +29,10 @@ namespace Emby.Server.Implementations.Udp
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
- private Socket _udpSocket;
- private IPEndPoint _endpoint;
private readonly byte[] _receiveBuffer = new byte[8192];
+ private Socket _udpSocket;
+ private IPEndPoint _endpoint;
private bool _disposed = false;
/// <summary>
@@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.Udp
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
- private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
+ private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken)
{
string? localUrl = _config[AddressOverrideConfigKey];
if (string.IsNullOrEmpty(localUrl))
@@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.Udp
try
{
- await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint).ConfigureAwait(false);
+ await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
}
catch (SocketException ex)
{
@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.Udp
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
- await RespondToV2Message(text, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
+ await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false);
}
}
catch (SocketException ex)
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index b0921cbd8..24d592525 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
+using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
@@ -10,8 +11,8 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
@@ -19,7 +20,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging;
@@ -47,7 +47,6 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
- private readonly IZipClient _zipClient;
private readonly object _currentInstallationsLock = new object();
/// <summary>
@@ -69,7 +68,6 @@ namespace Emby.Server.Implementations.Updates
/// <param name="eventManager">The <see cref="IEventManager"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
- /// <param name="zipClient">The <see cref="IZipClient"/>.</param>
/// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager(
ILogger<InstallationManager> logger,
@@ -78,7 +76,6 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
- IZipClient zipClient,
IPluginManager pluginManager)
{
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
@@ -90,7 +87,6 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_config = config;
- _zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.Options;
_pluginManager = pluginManager;
}
@@ -560,7 +556,8 @@ namespace Emby.Server.Implementations.Updates
}
stream.Position = 0;
- _zipClient.ExtractAllFromZip(stream, targetDir, true);
+ using var reader = new ZipArchive(stream);
+ reader.ExtractToDirectory(targetDir, true);
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
@@ -571,7 +568,7 @@ namespace Emby.Server.Implementations.Updates
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
- _logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
+ _logger.LogInformation("Plugin {Action}: {PluginName} {PluginVersion}", plugin == null ? "installed" : "updated", package.Name, package.Version);
return plugin != null;
}
diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
index 49b6689cd..58552d847 100644
--- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
@@ -1,4 +1,6 @@
-using System;
+#pragma warning disable CA1813 // Avoid unsealed attributes
+
+using System;
namespace Jellyfin.Api.Attributes
{
diff --git a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
index 001f27409..244a29da4 100644
--- a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
@@ -3,7 +3,7 @@
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
- public class AcceptsImageFileAttribute : AcceptsFileAttribute
+ public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
{
private const string ContentType = "image/*";
diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
index 2fdd1e489..af8727552 100644
--- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
+++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
@@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes
/// <summary>
/// Identifies an action that supports the HTTP GET method.
/// </summary>
- public class HttpSubscribeAttribute : HttpMethodAttribute
+ public sealed class HttpSubscribeAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
index d6d7e4563..1c0b70e71 100644
--- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
+++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
@@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes
/// <summary>
/// Identifies an action that supports the HTTP GET method.
/// </summary>
- public class HttpUnsubscribeAttribute : HttpMethodAttribute
+ public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
diff --git a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
index 56c9772b6..514e7ce97 100644
--- a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
+++ b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
@@ -6,7 +6,7 @@ namespace Jellyfin.Api.Attributes
/// Attribute to mark a parameter as obsolete.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
- public class ParameterObsoleteAttribute : Attribute
+ public sealed class ParameterObsoleteAttribute : Attribute
{
}
}
diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
index 3adb700eb..9fc25f192 100644
--- a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
@@ -3,7 +3,7 @@
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
- public class ProducesAudioFileAttribute : ProducesFileAttribute
+ public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
{
private const string ContentType = "audio/*";
diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
index 62a576ede..2bf77d729 100644
--- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs
@@ -1,4 +1,6 @@
-using System;
+#pragma warning disable CA1813 // Avoid unsealed attributes
+
+using System;
namespace Jellyfin.Api.Attributes
{
diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
index e15813676..1e5b542e2 100644
--- a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
@@ -3,7 +3,7 @@
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
- public class ProducesImageFileAttribute : ProducesFileAttribute
+ public sealed class ProducesImageFileAttribute : ProducesFileAttribute
{
private const string ContentType = "image/*";
diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
index 5d928ab91..5b15cb1a5 100644
--- a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
@@ -3,7 +3,7 @@
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
- public class ProducesPlaylistFileAttribute : ProducesFileAttribute
+ public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
{
private const string ContentType = "application/x-mpegURL";
diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
index d8b2856dc..6857d45ec 100644
--- a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
+++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
@@ -3,7 +3,7 @@
/// <summary>
/// Produces file attribute of "video/*".
/// </summary>
- public class ProducesVideoFileAttribute : ProducesFileAttribute
+ public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
{
private const string ContentType = "video/*";
diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
new file mode 100644
index 000000000..88af08dd3
--- /dev/null
+++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
@@ -0,0 +1,47 @@
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+
+namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
+{
+ /// <summary>
+ /// LAN access handler. Allows anonymous users.
+ /// </summary>
+ public class AnonymousLanAccessHandler : AuthorizationHandler<AnonymousLanAccessRequirement>
+ {
+ private readonly INetworkManager _networkManager;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AnonymousLanAccessHandler"/> class.
+ /// </summary>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ public AnonymousLanAccessHandler(
+ INetworkManager networkManager,
+ IHttpContextAccessor httpContextAccessor)
+ {
+ _networkManager = networkManager;
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ /// <inheritdoc />
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
+ {
+ var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress;
+
+ // Loopback will be on LAN, so we can accept null.
+ if (ip == null || _networkManager.IsInLocalNetwork(ip))
+ {
+ context.Succeed(requirement);
+ }
+ else
+ {
+ context.Fail();
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs
new file mode 100644
index 000000000..49af24ff3
--- /dev/null
+++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
+{
+ /// <summary>
+ /// The local network authorization requirement. Allows anonymous users.
+ /// </summary>
+ public class AnonymousLanAccessRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
index 392498c53..13d3257df 100644
--- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -1,4 +1,4 @@
-using System.Security.Claims;
+using System.Security.Claims;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index c56233794..bd3e7d9e3 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -40,11 +40,16 @@ namespace Jellyfin.Api.Auth
}
/// <inheritdoc />
- protected override Task<AuthenticateResult> HandleAuthenticateAsync()
+ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
try
{
- var authorizationInfo = _authService.Authenticate(Request);
+ var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false);
+ if (!authorizationInfo.HasToken)
+ {
+ return AuthenticateResult.NoResult();
+ }
+
var role = UserRoles.User;
if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
@@ -68,16 +73,16 @@ namespace Jellyfin.Api.Auth
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
- return Task.FromResult(AuthenticateResult.Success(ticket));
+ return AuthenticateResult.Success(ticket);
}
catch (AuthenticationException ex)
{
_logger.LogDebug(ex, "Error authenticating with {Handler}", nameof(CustomAuthenticationHandler));
- return Task.FromResult(AuthenticateResult.NoResult());
+ return AuthenticateResult.NoResult();
}
catch (SecurityException ex)
{
- return Task.FromResult(AuthenticateResult.Fail(ex));
+ return AuthenticateResult.Fail(ex);
}
}
}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
index 9815e252e..dd0bd4ec2 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs
@@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
}
/// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
- context.Succeed(firstTimeSetupOrDefaultRequirement);
+ context.Succeed(requirement);
return Task.CompletedTask;
}
var validated = ValidateClaims(context.User);
if (validated)
{
- context.Succeed(firstTimeSetupOrDefaultRequirement);
+ context.Succeed(requirement);
}
else
{
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index decbe0c03..90b76ee99 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -33,18 +33,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
}
/// <inheritdoc />
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement firstTimeSetupOrElevatedRequirement)
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
{
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
- context.Succeed(firstTimeSetupOrElevatedRequirement);
+ context.Succeed(requirement);
return Task.CompletedTask;
}
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
- context.Succeed(firstTimeSetupOrElevatedRequirement);
+ context.Succeed(requirement);
}
else
{
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
index b898ac76c..e6c04eb08 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
- || _syncPlayManager.IsUserActive(userId!.Value))
+ || _syncPlayManager.IsUserActive(userId.Value))
{
context.Succeed(requirement);
}
@@ -85,7 +85,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
- if (_syncPlayManager.IsUserActive(userId!.Value))
+ if (_syncPlayManager.IsUserActive(userId.Value))
{
context.Succeed(requirement);
}
diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 1c1fc71d7..59d6b7513 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,5 +1,5 @@
using System.Net.Mime;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 632dedb3c..a72eeea28 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -46,6 +46,11 @@ namespace Jellyfin.Api.Constants
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
/// <summary>
+ /// Policy name for requiring (anonymous) LAN access.
+ /// </summary>
+ public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
+
+ /// <summary>
/// Policy name for escaping schedule controls or requiring first time setup.
/// </summary>
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index b429cebec..ae45f647f 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
{
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{
- StartIndex = startIndex,
+ Skip = startIndex,
Limit = limit,
MinDate = minDate,
HasUserId = hasUserId
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 8c43d786a..8e0332d3e 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -1,10 +1,7 @@
-using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -18,24 +15,15 @@ namespace Jellyfin.Api.Controllers
[Route("Auth")]
public class ApiKeyController : BaseJellyfinApiController
{
- private readonly ISessionManager _sessionManager;
- private readonly IServerApplicationHost _appHost;
- private readonly IAuthenticationRepository _authRepo;
+ private readonly IAuthenticationManager _authenticationManager;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
/// </summary>
- /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
- /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
- public ApiKeyController(
- ISessionManager sessionManager,
- IServerApplicationHost appHost,
- IAuthenticationRepository authRepo)
+ /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
+ public ApiKeyController(IAuthenticationManager authenticationManager)
{
- _sessionManager = sessionManager;
- _appHost = appHost;
- _authRepo = authRepo;
+ _authenticationManager = authenticationManager;
}
/// <summary>
@@ -46,14 +34,15 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
+ public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
{
- var result = _authRepo.Get(new AuthenticationInfoQuery
- {
- HasUser = false
- });
+ var keys = await _authenticationManager.GetApiKeys();
- return result;
+ return new QueryResult<AuthenticationInfo>
+ {
+ Items = keys,
+ TotalRecordCount = keys.Count
+ };
}
/// <summary>
@@ -65,17 +54,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateKey([FromQuery, Required] string app)
+ public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
{
- _authRepo.Create(new AuthenticationInfo
- {
- AppName = app,
- AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- DateCreated = DateTime.UtcNow,
- DeviceId = _appHost.SystemId,
- DeviceName = _appHost.FriendlyName,
- AppVersion = _appHost.ApplicationVersionString
- });
+ await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
+
return NoContent();
}
@@ -88,9 +70,10 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Keys/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RevokeKey([FromRoute, Required] string key)
+ public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
{
- _sessionManager.RevokeToken(key);
+ await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
+
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 154a56702..3df975563 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -133,8 +133,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypes,
StartIndex = startIndex,
Limit = limit,
@@ -337,8 +337,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypes,
StartIndex = startIndex,
Limit = limit,
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index a6e70e72d..54ac06276 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs
new file mode 100644
index 000000000..98fd22430
--- /dev/null
+++ b/Jellyfin.Api/Controllers/ClientLogController.cs
@@ -0,0 +1,80 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.ClientLogDtos;
+using MediaBrowser.Controller.ClientEvent;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Client log controller.
+ /// </summary>
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ public class ClientLogController : BaseJellyfinApiController
+ {
+ private const int MaxDocumentSize = 1_000_000;
+ private readonly IClientEventLogger _clientEventLogger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClientLogController"/> class.
+ /// </summary>
+ /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ClientLogController(
+ IClientEventLogger clientEventLogger,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _clientEventLogger = clientEventLogger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <summary>
+ /// Upload a document.
+ /// </summary>
+ /// <response code="200">Document saved.</response>
+ /// <response code="403">Event logging disabled.</response>
+ /// <response code="413">Upload size too large.</response>
+ /// <returns>Create response.</returns>
+ [HttpPost("Document")]
+ [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+ [AcceptsFile(MediaTypeNames.Text.Plain)]
+ [RequestSizeLimit(MaxDocumentSize)]
+ public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
+ {
+ if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
+ {
+ return Forbid();
+ }
+
+ if (Request.ContentLength > MaxDocumentSize)
+ {
+ // Manually validate to return proper status code.
+ return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
+ }
+
+ var (clientName, clientVersion) = GetRequestInformation();
+ var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
+ .ConfigureAwait(false);
+ return Ok(new ClientLogDocumentResponseDto(fileName));
+ }
+
+ private (string ClientName, string ClientVersion) GetRequestInformation()
+ {
+ var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client";
+ var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User)
+ ? "apikey"
+ : ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version";
+
+ return (clientName, clientVersion);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 852d1e9cb..8a98d856c 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
- var userId = _authContext.GetAuthorizationInfo(Request).UserId;
+ var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId;
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index b6309baab..60529e990 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 445733c24..87cb418d9 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
if (enableInMainMenu.HasValue)
{
- configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
+ configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
}
return configPages;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index b3e3490c2..8292cf83b 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,8 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Data.Dtos;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
@@ -19,22 +22,18 @@ namespace Jellyfin.Api.Controllers
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
- private readonly IAuthenticationRepository _authenticationRepository;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="DevicesController"/> class.
/// </summary>
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
- /// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
public DevicesController(
IDeviceManager deviceManager,
- IAuthenticationRepository authenticationRepository,
ISessionManager sessionManager)
{
_deviceManager = deviceManager;
- _authenticationRepository = authenticationRepository;
_sessionManager = sessionManager;
}
@@ -47,10 +46,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+ public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
- var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
- return _deviceManager.GetDevices(deviceQuery);
+ return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
}
/// <summary>
@@ -63,9 +61,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
+ public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
{
- var deviceInfo = _deviceManager.GetDevice(id);
+ var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
if (deviceInfo == null)
{
return NotFound();
@@ -84,9 +82,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
+ public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
{
- var deviceInfo = _deviceManager.GetDeviceOptions(id);
+ var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
if (deviceInfo == null)
{
return NotFound();
@@ -101,22 +99,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">Device Id.</param>
/// <param name="deviceOptions">Device Options.</param>
/// <response code="204">Device options updated.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Options")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateDeviceOptions(
+ public async Task<ActionResult> UpdateDeviceOptions(
[FromQuery, Required] string id,
- [FromBody, Required] DeviceOptions deviceOptions)
+ [FromBody, Required] DeviceOptionsDto deviceOptions)
{
- var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
- if (existingDeviceOptions == null)
- {
- return NotFound();
- }
-
- _deviceManager.UpdateDeviceOptions(id, deviceOptions);
+ await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
return NoContent();
}
@@ -130,19 +120,19 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteDevice([FromQuery, Required] string id)
+ public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
{
- var existingDevice = _deviceManager.GetDevice(id);
+ var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
if (existingDevice == null)
{
return NotFound();
}
- var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
+ var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
- foreach (var session in sessions)
+ foreach (var session in sessions.Items)
{
- _sessionManager.Logout(session);
+ await _sessionManager.Logout(session).ConfigureAwait(false);
}
return NoContent();
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 87b4577b6..0b2604640 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -8,7 +8,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Dto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -137,27 +137,30 @@ namespace Jellyfin.Api.Controllers
}
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+ existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
+ && !string.IsNullOrEmpty(chromecastVersion)
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
: ChromecastVersion.Stable;
displayPreferences.CustomPrefs.Remove("chromecastVersion");
- existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
- ? bool.Parse(enableNextVideoInfoOverlay)
- : true;
+ existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
+ || string.IsNullOrEmpty(enableNextVideoInfoOverlay)
+ || bool.Parse(enableNextVideoInfoOverlay);
displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
+ && !string.IsNullOrEmpty(skipBackLength)
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
: 10000;
displayPreferences.CustomPrefs.Remove("skipBackLength");
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
+ && !string.IsNullOrEmpty(skipForwardLength)
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
: 30000;
displayPreferences.CustomPrefs.Remove("skipForwardLength");
@@ -196,7 +199,7 @@ namespace Jellyfin.Api.Controllers
}
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
- itemPrefs.SortBy = displayPreferences.SortBy;
+ itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName";
itemPrefs.SortOrder = displayPreferences.SortOrder;
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index 052a6aff2..35c3a3d92 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- _dlnaManager.UpdateProfile(deviceProfile);
+ _dlnaManager.UpdateProfile(profileId, deviceProfile);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 694d16ad9..4e8c01577 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -7,7 +7,9 @@ using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
/// Dlna Server Controller.
/// </summary>
[Route("Dlna")]
+ [Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
public class DlnaServerController : BaseJellyfinApiController
{
private readonly IDlnaManager _dlnaManager;
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 62283d038..caa3d2368 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -316,7 +315,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -482,7 +481,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -813,7 +812,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -1380,7 +1379,7 @@ namespace Jellyfin.Api.Controllers
}
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
{
- var outputFmp4HeaderArg = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) switch
+ var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
{
// on Windows, the path of fmp4 header file needs to be configured
true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
@@ -1392,7 +1391,7 @@ namespace Jellyfin.Api.Controllers
}
else
{
- _logger.LogError("Invalid HLS segment container: " + segmentFormat);
+ _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
}
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
@@ -1496,7 +1495,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
return args;
}
@@ -1710,7 +1709,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext);
}
private long GetEndPositionTicks(StreamState state, int requestedIndex)
@@ -1795,7 +1794,7 @@ namespace Jellyfin.Api.Controllers
return;
}
- _logger.LogDebug("Deleting partial HLS file {path}", path);
+ _logger.LogDebug("Deleting partial HLS file {Path}", path);
try
{
@@ -1803,15 +1802,15 @@ namespace Jellyfin.Api.Controllers
}
catch (IOException ex)
{
- _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
var task = Task.Delay(100);
- Task.WaitAll(task);
+ task.Wait();
DeleteFile(path, retryCount + 1);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
}
}
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 223b2a2b6..e170436d1 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using Jellyfin.Api.Constants;
-using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -71,7 +70,7 @@ namespace Jellyfin.Api.Controllers
{
User = user,
MediaTypes = mediaTypes,
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ IncludeItemTypes = includeItemTypes,
Recursive = true,
EnableTotalRecordCount = false,
DtoOptions = new DtoOptions
@@ -166,7 +165,7 @@ namespace Jellyfin.Api.Controllers
var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ IncludeItemTypes = includeItemTypes,
DtoOptions = new DtoOptions
{
Fields = Array.Empty<ItemFields>(),
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 5aa457153..37e6ae184 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
Genre item = new Genre();
if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
{
- var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions);
+ var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
if (result != null)
{
@@ -182,27 +182,27 @@ namespace Jellyfin.Api.Controllers
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+ private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
where T : BaseItem, new()
{
var result = libraryManager.GetItemList(new InternalItemsQuery
{
Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { baseItemKind },
DtoOptions = dtoOptions
}).OfType<T>().FirstOrDefault();
result ??= libraryManager.GetItemList(new InternalItemsQuery
{
Name = name.Replace(BaseItem.SlugChar, '/'),
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { baseItemKind },
DtoOptions = dtoOptions
}).OfType<T>().FirstOrDefault();
result ??= libraryManager.GetItemList(new InternalItemsQuery
{
Name = name.Replace(BaseItem.SlugChar, '?'),
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { baseItemKind },
DtoOptions = dtoOptions
}).OfType<T>().FirstOrDefault();
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 473bdc523..7325dca0a 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -64,12 +64,12 @@ namespace Jellyfin.Api.Controllers
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
{
return BadRequest("Invalid segment.");
}
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext);
}
/// <summary>
@@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
{
return BadRequest("Invalid segment.");
}
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
{
return BadRequest("Invalid segment.");
}
@@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index e1b808098..89bbf22c9 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -82,13 +82,13 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- if (!path.StartsWith(_applicationPaths.GeneralPath))
+ if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return File(AsyncFile.OpenRead(path), contentType);
}
/// <summary>
@@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
- if (!path.StartsWith(basePath))
+ if (!path.StartsWith(basePath, StringComparison.InvariantCulture))
{
return BadRequest("Invalid image path.");
}
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
- if (!path.StartsWith(basePath))
+ if (!path.StartsWith(basePath, StringComparison.InvariantCulture))
{
return BadRequest("Invalid image path.");
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 8f7500ac6..86933074d 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
@@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
@@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
@@ -341,7 +341,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -377,7 +377,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -2007,7 +2007,7 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(HeaderNames.CacheControl, "public");
}
- Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
+ Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
@@ -2026,7 +2026,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- return PhysicalFile(imagePath, imageContentType);
+ return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index f232dffaa..a6c2e07c9 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -80,7 +80,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -223,43 +223,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
-
- /// <summary>
- /// Creates an instant playlist based on a given genre.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <response code="200">Instant playlist returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("MusicGenres/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
- [FromRoute, Required] Guid id,
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableImages,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
- {
- var item = _libraryManager.GetItemById(id);
- var user = userId.HasValue && !userId.Equals(Guid.Empty)
- ? _userManager.GetUserById(userId.Value)
- : null;
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -295,7 +259,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -352,8 +316,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [Obsolete("Use GetInstantMixFromMusicGenres instead")]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -363,15 +326,15 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- return GetInstantMixFromMusicGenreById(
- id,
- userId,
- limit,
- fields,
- enableImages,
- enableUserData,
- imageTypeLimit,
- enableImageTypes);
+ var item = _libraryManager.GetItemById(id);
+ var user = userId.HasValue && !userId.Equals(Guid.Empty)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(Request)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
}
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 9fa307858..8a6f9b8c7 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -1,15 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -17,7 +12,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -34,7 +28,6 @@ namespace Jellyfin.Api.Controllers
public class ItemLookupController : BaseJellyfinApiController
{
private readonly IProviderManager _providerManager;
- private readonly IServerApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<ItemLookupController> _logger;
@@ -43,19 +36,16 @@ namespace Jellyfin.Api.Controllers
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
public ItemLookupController(
IProviderManager providerManager,
- IServerConfigurationManager serverConfigurationManager,
IFileSystem fileSystem,
ILibraryManager libraryManager,
ILogger<ItemLookupController> logger)
{
_providerManager = providerManager;
- _appPaths = serverConfigurationManager.ApplicationPaths;
_fileSystem = fileSystem;
_libraryManager = libraryManager;
_logger = logger;
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index a9f4a5a58..fd137f98f 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -154,11 +154,11 @@ namespace Jellyfin.Api.Controllers
};
if (!item.IsVirtualItem
- && !(item is ICollectionFolder)
- && !(item is UserView)
- && !(item is AggregateFolder)
- && !(item is LiveTvChannel)
- && !(item is IItemByName)
+ && item is not ICollectionFolder
+ && item is not UserView
+ && item is not AggregateFolder
+ && item is not LiveTvChannel
+ && item is not IItemByName
&& item.SourceType == SourceType.Library)
{
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
@@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
}
- item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
- item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
+ item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
+ item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
item.ProductionYear = request.ProductionYear;
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
item.CustomRating = request.CustomRating;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 35c27dd0e..65c0662d2 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -8,8 +8,8 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -33,6 +33,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILocalizationManager _localization;
private readonly IDtoService _dtoService;
private readonly ILogger<ItemsController> _logger;
+ private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsController"/> class.
@@ -42,18 +43,21 @@ namespace Jellyfin.Api.Controllers
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
public ItemsController(
IUserManager userManager,
ILibraryManager libraryManager,
ILocalizationManager localization,
IDtoService dtoService,
- ILogger<ItemsController> logger)
+ ILogger<ItemsController> logger,
+ ISessionManager sessionManager)
{
_userManager = userManager;
_libraryManager = libraryManager;
_localization = localization;
_dtoService = dtoService;
_logger = logger;
+ _sessionManager = sessionManager;
}
/// <summary>
@@ -241,7 +245,7 @@ namespace Jellyfin.Api.Controllers
var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result;
- if (!(item is Folder folder))
+ if (item is not Folder folder)
{
folder = _libraryManager.GetUserRootFolder();
}
@@ -285,14 +289,14 @@ namespace Jellyfin.Api.Controllers
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
}
- if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
+ if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
{
- var query = new InternalItemsQuery(user!)
+ var query = new InternalItemsQuery(user)
{
IsPlayed = isPlayed,
MediaTypes = mediaTypes,
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite,
@@ -454,7 +458,7 @@ namespace Jellyfin.Api.Controllers
{
query.AlbumIds = albums.SelectMany(i =>
{
- return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
+ return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 });
}).ToArray();
}
@@ -478,7 +482,7 @@ namespace Jellyfin.Api.Controllers
if (query.OrderBy.Count == 0)
{
// Albums by artist
- if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
+ if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
{
query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
}
@@ -763,6 +767,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
/// <response code="200">Items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
[HttpGet("Users/{userId}/Items/Resume")]
@@ -781,7 +786,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool excludeActiveSessions = false)
{
var user = _userManager.GetUserById(userId);
var parentIdGuid = parentId ?? Guid.Empty;
@@ -801,6 +807,15 @@ namespace Jellyfin.Api.Controllers
.ToArray();
}
+ var excludeItemIds = Array.Empty<Guid>();
+ if (excludeActiveSessions)
+ {
+ excludeItemIds = _sessionManager.Sessions
+ .Where(s => s.UserId == userId && s.NowPlayingItem != null)
+ .Select(s => s.NowPlayingItem.Id)
+ .ToArray();
+ }
+
var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
@@ -815,9 +830,10 @@ namespace Jellyfin.Api.Controllers
CollapseBoxSetItems = false,
EnableTotalRecordCount = enableTotalRecordCount,
AncestorIds = ancestorIds,
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- SearchTerm = searchTerm
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ SearchTerm = searchTerm,
+ ExcludeItemIds = excludeItemIds
});
var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index ab6b0312d..f1b9c2f67 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -14,6 +14,8 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -22,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Activity;
@@ -36,7 +37,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-using Book = MediaBrowser.Controller.Entities.Book;
namespace Jellyfin.Api.Controllers
{
@@ -331,10 +331,10 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItem(Guid itemId)
+ public async Task<ActionResult> DeleteItem(Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
- var auth = _authContext.GetAuthorizationInfo(Request);
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
if (!item.CanDelete(user))
@@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ public async Task<ActionResult> DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
if (ids.Length == 0)
{
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
- var auth = _authContext.GetAuthorizationInfo(Request);
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
if (!item.CanDelete(user))
@@ -413,14 +413,14 @@ namespace Jellyfin.Api.Controllers
var counts = new ItemCounts
{
- AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite),
- EpisodeCount = GetCount(typeof(Episode), user, isFavorite),
- MovieCount = GetCount(typeof(Movie), user, isFavorite),
- SeriesCount = GetCount(typeof(Series), user, isFavorite),
- SongCount = GetCount(typeof(Audio), user, isFavorite),
- MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite),
- BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite),
- BookCount = GetCount(typeof(Book), user, isFavorite)
+ AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
+ EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
+ MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
+ SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
+ SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
+ MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
+ BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
+ BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
};
return counts;
@@ -529,7 +529,7 @@ namespace Jellyfin.Api.Controllers
{
var series = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Series) },
+ IncludeItemTypes = new[] { BaseItemKind.Series },
DtoOptions = new DtoOptions(false)
{
EnableImages = false
@@ -559,7 +559,7 @@ namespace Jellyfin.Api.Controllers
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Movie) },
+ IncludeItemTypes = new[] { BaseItemKind.Movie },
DtoOptions = new DtoOptions(false)
{
EnableImages = false
@@ -627,7 +627,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- var auth = _authContext.GetAuthorizationInfo(Request);
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
@@ -700,7 +700,7 @@ namespace Jellyfin.Api.Controllers
: _libraryManager.RootFolder)
: _libraryManager.GetItemById(itemId);
- if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
+ if (item is Episode || (item is IItemByName && item is not MusicArtist))
{
return new QueryResult<BaseItemDto>();
}
@@ -715,30 +715,31 @@ namespace Jellyfin.Api.Controllers
bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
bool? isSeries = item is Series || (program != null && program.IsSeries);
- var includeItemTypes = new List<string>();
+ var includeItemTypes = new List<BaseItemKind>();
if (isMovie.Value)
{
- includeItemTypes.Add(nameof(Movie));
+ includeItemTypes.Add(BaseItemKind.Movie);
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- includeItemTypes.Add(nameof(Trailer));
- includeItemTypes.Add(nameof(LiveTvProgram));
+ includeItemTypes.Add(BaseItemKind.Trailer);
+ includeItemTypes.Add(BaseItemKind.LiveTvProgram);
}
}
else if (isSeries.Value)
{
- includeItemTypes.Add(nameof(Series));
+ includeItemTypes.Add(BaseItemKind.Series);
}
else
{
// For non series and movie types these columns are typically null
// isSeries = null;
isMovie = null;
- includeItemTypes.Add(item.GetType().Name);
+ includeItemTypes.Add(item.GetBaseItemKind());
}
var query = new InternalItemsQuery(user)
{
+ Genres = item.Genres,
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
SimilarTo = item,
@@ -785,7 +786,7 @@ namespace Jellyfin.Api.Controllers
var typesList = types.ToList();
var plugins = _providerManager.GetAllMetadataPlugins()
- .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
+ .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
.OrderBy(i => typesList.IndexOf(i.ItemType))
.ToList();
@@ -871,11 +872,11 @@ namespace Jellyfin.Api.Controllers
return result;
}
- private int GetCount(Type type, User? user, bool? isFavorite)
+ private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
{
var query = new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { type.Name },
+ IncludeItemTypes = new[] { itemKind },
Limit = 0,
Recursive = true,
IsVirtualItem = false,
@@ -940,10 +941,10 @@ namespace Jellyfin.Api.Controllers
}
var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
.ToArray();
- return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
+ return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
}
private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
@@ -967,7 +968,7 @@ namespace Jellyfin.Api.Controllers
.ToArray();
return metadataOptions.Length == 0
- || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+ || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
}
private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
@@ -997,7 +998,7 @@ namespace Jellyfin.Api.Controllers
return true;
}
- return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+ return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
}
}
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index be9127dd3..ec1170411 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
if (paths != null && paths.Length > 0)
{
- libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
+ libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
}
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
@@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
try
{
- var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path };
+ var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
_libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 24ee833ef..b131530c9 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
@@ -429,10 +428,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult ResetTuner([FromRoute, Required] string tunerId)
+ public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
{
- AssertUserCanManageLiveTv();
- _liveTvManager.ResetTuner(tunerId, CancellationToken.None);
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
+ await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -761,9 +760,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
+ public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
var item = _libraryManager.GetItemById(recordingId);
if (item == null)
@@ -790,7 +789,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
return NoContent();
}
@@ -808,7 +807,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -824,7 +823,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -882,7 +881,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
return NoContent();
}
@@ -900,7 +899,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -916,7 +915,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
- AssertUserCanManageLiveTv();
+ await AssertUserCanManageLiveTv().ConfigureAwait(false);
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -1172,7 +1171,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
- public async Task<ActionResult> GetLiveRecordingFile([FromRoute, Required] string recordingId)
+ public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
{
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
@@ -1181,11 +1180,8 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- await using var memoryStream = new MemoryStream();
- await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(memoryStream, CancellationToken.None)
- .ConfigureAwait(false);
- return File(memoryStream, MimeTypes.GetMimeType(path));
+ var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
}
/// <summary>
@@ -1203,21 +1199,21 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
- public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
+ public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
{
- var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo == null)
{
return NotFound();
}
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
- private void AssertUserCanManageLiveTv()
+ private async Task AssertUserCanManageLiveTv()
{
- var user = _sessionContext.GetUser(Request);
+ var user = await _sessionContext.GetUser(Request).ConfigureAwait(false);
if (user == null)
{
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index e330f02b6..b422eb78c 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
- var authInfo = _authContext.GetAuthorizationInfo(Request);
+ var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var profile = playbackInfoDto?.DeviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
@@ -161,6 +161,11 @@ namespace Jellyfin.Api.Controllers
liveStreamId)
.ConfigureAwait(false);
+ if (info.ErrorCode != null)
+ {
+ return info;
+ }
+
if (profile != null)
{
// set device specific data
@@ -179,7 +184,7 @@ namespace Jellyfin.Api.Controllers
audioStreamIndex,
subtitleStreamIndex,
maxAudioChannels,
- info!.PlaySessionId!,
+ info.PlaySessionId!,
userId ?? Guid.Empty,
enableDirectPlay.Value,
enableDirectStream.Value,
@@ -302,31 +307,16 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="size">The bitrate. Defaults to 102400.</param>
/// <response code="200">Test buffer returned.</response>
- /// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
/// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
[HttpGet("Playback/BitrateTest")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [Produces(MediaTypeNames.Application.Octet)]
[ProducesFile(MediaTypeNames.Application.Octet)]
- public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
+ public ActionResult GetBitrateTestBytes([FromQuery][Range(1, 100_000_000, ErrorMessage = "The requested size must be greater than or equal to {1} and less than or equal to {2}")] int size = 102400)
{
- const int MaxSize = 10_000_000;
-
- if (size <= 0)
- {
- return BadRequest($"The requested size ({size}) is equal to or smaller than 0.");
- }
-
- if (size > MaxSize)
- {
- return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).");
- }
-
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
- new Random().NextBytes(buffer);
+ Random.Shared.NextBytes(buffer);
return File(buffer, MediaTypeNames.Application.Octet);
}
finally
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 010a3b19a..db72ff2f8 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -11,14 +11,11 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
@@ -85,7 +82,7 @@ namespace Jellyfin.Api.Controllers
{
IncludeItemTypes = new[]
{
- nameof(Movie),
+ BaseItemKind.Movie,
// nameof(Trailer),
// nameof(LiveTvProgram)
},
@@ -100,11 +97,11 @@ namespace Jellyfin.Api.Controllers
var recentlyPlayedMovies = _libraryManager.GetItemList(query);
- var itemTypes = new List<string> { nameof(Movie) };
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- itemTypes.Add(nameof(Trailer));
- itemTypes.Add(nameof(LiveTvProgram));
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
}
var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
@@ -183,11 +180,11 @@ namespace Jellyfin.Api.Controllers
DtoOptions dtoOptions,
RecommendationType type)
{
- var itemTypes = new List<string> { nameof(Movie) };
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- itemTypes.Add(nameof(Trailer));
- itemTypes.Add(nameof(LiveTvProgram));
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
}
foreach (var name in names)
@@ -225,11 +222,11 @@ namespace Jellyfin.Api.Controllers
private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
{
- var itemTypes = new List<string> { nameof(Movie) };
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- itemTypes.Add(nameof(Trailer));
- itemTypes.Add(nameof(LiveTvProgram));
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
}
foreach (var name in names)
@@ -265,11 +262,11 @@ namespace Jellyfin.Api.Controllers
private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
{
- var itemTypes = new List<string> { nameof(Movie) };
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
- itemTypes.Add(nameof(Trailer));
- itemTypes.Add(nameof(LiveTvProgram));
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
}
foreach (var item in baselineItems)
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 27eec2b9a..c4c03aa4f 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -149,7 +149,7 @@ namespace Jellyfin.Api.Controllers
if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
{
- item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions);
+ item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
}
else
{
@@ -166,27 +166,27 @@ namespace Jellyfin.Api.Controllers
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+ private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
where T : BaseItem, new()
{
var result = libraryManager.GetItemList(new InternalItemsQuery
{
Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { baseItemKind },
DtoOptions = dtoOptions
}).OfType<T>().FirstOrDefault();
result ??= libraryManager.GetItemList(new InternalItemsQuery
{
Name = name.Replace(BaseItem.SlugChar, '/'),
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { baseItemKind },
DtoOptions = dtoOptions
}).OfType<T>().FirstOrDefault();
result ??= libraryManager.GetItemList(new InternalItemsQuery
{
Name = name.Replace(BaseItem.SlugChar, '?'),
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { baseItemKind },
DtoOptions = dtoOptions
}).OfType<T>().FirstOrDefault();
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index b98307f87..cb4894d77 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -26,7 +26,6 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataManager;
/// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class.
@@ -34,17 +33,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
public PersonsController(
ILibraryManager libraryManager,
IDtoService dtoService,
- IUserManager userManager,
- IUserDataManager userDataManager)
+ IUserManager userManager)
{
_libraryManager = libraryManager;
_dtoService = dtoService;
_userManager = userManager;
- _userDataManager = userDataManager;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index f256c8c25..6dee1c219 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -72,13 +72,13 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkPlayedItem(
+ public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
- var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
@@ -98,10 +98,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
- var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var dto = UpdatePlayedStatus(user, itemId, false, null);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
{
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
- playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
return NoContent();
}
@@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
{
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
- playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
@@ -171,10 +171,11 @@ namespace Jellyfin.Api.Controllers
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
- playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}
@@ -220,7 +221,7 @@ namespace Jellyfin.Api.Controllers
};
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
- playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
return NoContent();
}
@@ -278,7 +279,7 @@ namespace Jellyfin.Api.Controllers
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
- playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
@@ -320,10 +321,11 @@ namespace Jellyfin.Api.Controllers
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
- playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 7a6130719..b41df1abb 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -8,8 +8,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Net;
@@ -28,7 +27,6 @@ namespace Jellyfin.Api.Controllers
{
private readonly IInstallationManager _installationManager;
private readonly IPluginManager _pluginManager;
- private readonly IConfigurationManager _config;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
@@ -36,16 +34,13 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
- /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public PluginsController(
IInstallationManager installationManager,
- IPluginManager pluginManager,
- IConfigurationManager config)
+ IPluginManager pluginManager)
{
_installationManager = installationManager;
_pluginManager = pluginManager;
_serializerOptions = JsonDefaults.Options;
- _config = config;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 4ac849181..87b78fe93 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -1,7 +1,10 @@
using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Model.QuickConnect;
using Microsoft.AspNetCore.Authorization;
@@ -16,27 +19,29 @@ namespace Jellyfin.Api.Controllers
public class QuickConnectController : BaseJellyfinApiController
{
private readonly IQuickConnect _quickConnect;
+ private readonly IAuthorizationContext _authContext;
/// <summary>
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
/// </summary>
/// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
- public QuickConnectController(IQuickConnect quickConnect)
+ /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
{
_quickConnect = quickConnect;
+ _authContext = authContext;
}
/// <summary>
/// Gets the current quick connect state.
/// </summary>
/// <response code="200">Quick connect state returned.</response>
- /// <returns>The current <see cref="QuickConnectState"/>.</returns>
- [HttpGet("Status")]
+ /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
+ [HttpGet("Enabled")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QuickConnectState> GetStatus()
+ public ActionResult<bool> GetEnabled()
{
- _quickConnect.ExpireRequests();
- return _quickConnect.State;
+ return _quickConnect.IsEnabled;
}
/// <summary>
@@ -47,9 +52,17 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
[HttpGet("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QuickConnectResult> Initiate()
+ public async Task<ActionResult<QuickConnectResult>> Initiate()
{
- return _quickConnect.TryConnect();
+ try
+ {
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ return _quickConnect.TryConnect(auth);
+ }
+ catch (AuthenticationException)
+ {
+ return Unauthorized("Quick connect is disabled");
+ }
}
/// <summary>
@@ -72,42 +85,10 @@ namespace Jellyfin.Api.Controllers
{
return NotFound("Unknown secret");
}
- }
-
- /// <summary>
- /// Temporarily activates quick connect for five minutes.
- /// </summary>
- /// <response code="204">Quick connect has been temporarily activated.</response>
- /// <response code="403">Quick connect is unavailable on this server.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("Activate")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult Activate()
- {
- if (_quickConnect.State == QuickConnectState.Unavailable)
+ catch (AuthenticationException)
{
- return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
+ return Unauthorized("Quick connect is disabled");
}
-
- _quickConnect.Activate();
- return NoContent();
- }
-
- /// <summary>
- /// Enables or disables quick connect.
- /// </summary>
- /// <param name="status">New <see cref="QuickConnectState"/>.</param>
- /// <response code="204">Quick connect state set successfully.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("Available")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
- {
- _quickConnect.SetState(status);
- return NoContent();
}
/// <summary>
@@ -121,7 +102,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult<bool> Authorize([FromQuery, Required] string code)
+ public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code)
{
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue)
@@ -129,26 +110,14 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
}
- return _quickConnect.AuthorizeRequest(userId.Value, code);
- }
-
- /// <summary>
- /// Deauthorize all quick connect devices for the current user.
- /// </summary>
- /// <response code="200">All quick connect devices were deleted.</response>
- /// <returns>The number of devices that were deleted.</returns>
- [HttpPost("Deauthorize")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<int> Deauthorize()
- {
- var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
- if (!userId.HasValue)
+ try
{
- return 0;
+ return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
+ }
+ catch (AuthenticationException)
+ {
+ return Unauthorized("Quick connect is disabled");
}
-
- return _quickConnect.DeleteAllDevices(userId.Value);
}
}
}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index ec836f43e..9f57a5cdb 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -3,20 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -32,7 +25,6 @@ namespace Jellyfin.Api.Controllers
{
private readonly IProviderManager _providerManager;
private readonly IServerApplicationPaths _applicationPaths;
- private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
@@ -40,17 +32,14 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
- /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public RemoteImageController(
IProviderManager providerManager,
IServerApplicationPaths applicationPaths,
- IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
{
_providerManager = providerManager;
_applicationPaths = applicationPaths;
- _httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
}
@@ -185,36 +174,5 @@ namespace Jellyfin.Api.Controllers
{
return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
-
- /// <summary>
- /// Downloads the image.
- /// </summary>
- /// <param name="url">The URL.</param>
- /// <param name="urlHash">The URL hash.</param>
- /// <param name="pointerCachePath">The pointer cache path.</param>
- /// <returns>Task.</returns>
- private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
- {
- var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
- if (response.Content.Headers.ContentType?.MediaType == null)
- {
- throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
- }
-
- var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
- Directory.CreateDirectory(fullCacheDirectory);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
- await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
-
- var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
- Directory.CreateDirectory(pointerCacheDirectory);
- await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
- .ConfigureAwait(false);
- }
}
}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 73bdf9018..26acb4cdc 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
-using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Drawing;
@@ -110,8 +109,8 @@ namespace Jellyfin.Api.Controllers
IncludeStudios = includeStudios,
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
MediaTypes = mediaTypes,
ParentId = parentId,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 7bd0b6918..a6bbd40cc 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -124,9 +125,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Viewing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DisplayContent(
+ public async Task<ActionResult> DisplayContent(
[FromRoute, Required] string sessionId,
- [FromQuery, Required] string itemType,
+ [FromQuery, Required] BaseItemKind itemType,
[FromQuery, Required] string itemId,
[FromQuery, Required] string itemName)
{
@@ -137,11 +138,12 @@ namespace Jellyfin.Api.Controllers
ItemType = itemType
};
- _sessionManager.SendBrowseCommand(
- RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
+ await _sessionManager.SendBrowseCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
command,
- CancellationToken.None);
+ CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -162,7 +164,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Playing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult Play(
+ public async Task<ActionResult> Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
@@ -183,11 +185,12 @@ namespace Jellyfin.Api.Controllers
StartIndex = startIndex
};
- _sessionManager.SendPlayCommand(
- RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
+ await _sessionManager.SendPlayCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
playRequest,
- CancellationToken.None);
+ CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -204,14 +207,14 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SendPlaystateCommand(
+ public async Task<ActionResult> SendPlaystateCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] PlaystateCommand command,
[FromQuery] long? seekPositionTicks,
[FromQuery] string? controllingUserId)
{
- _sessionManager.SendPlaystateCommand(
- RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
+ await _sessionManager.SendPlaystateCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
sessionId,
new PlaystateRequest()
{
@@ -219,7 +222,8 @@ namespace Jellyfin.Api.Controllers
ControllingUserId = controllingUserId,
SeekPositionTicks = seekPositionTicks,
},
- CancellationToken.None);
+ CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -234,18 +238,18 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/System/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SendSystemCommand(
+ public async Task<ActionResult> SendSystemCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] GeneralCommandType command)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var generalCommand = new GeneralCommand
{
Name = command,
ControllingUserId = currentSession.UserId
};
- _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
+ await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
return NoContent();
}
@@ -260,11 +264,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Command/{command}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SendGeneralCommand(
+ public async Task<ActionResult> SendGeneralCommand(
[FromRoute, Required] string sessionId,
[FromRoute, Required] GeneralCommandType command)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
var generalCommand = new GeneralCommand
{
@@ -272,7 +276,8 @@ namespace Jellyfin.Api.Controllers
ControllingUserId = currentSession.UserId
};
- _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
+ await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -287,11 +292,12 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Command")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SendFullGeneralCommand(
+ public async Task<ActionResult> SendFullGeneralCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] GeneralCommand command)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request)
+ .ConfigureAwait(false);
if (command == null)
{
@@ -300,11 +306,12 @@ namespace Jellyfin.Api.Controllers
command.ControllingUserId = currentSession.UserId;
- _sessionManager.SendGeneralCommand(
+ await _sessionManager.SendGeneralCommand(
currentSession.Id,
sessionId,
command,
- CancellationToken.None);
+ CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -319,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Message")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SendMessageCommand(
+ public async Task<ActionResult> SendMessageCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] MessageCommand command)
{
@@ -328,7 +335,12 @@ namespace Jellyfin.Api.Controllers
command.Header = "Message from Server";
}
- _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
+ await _sessionManager.SendMessageCommand(
+ await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
+ sessionId,
+ command,
+ CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -383,7 +395,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Capabilities")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostCapabilities(
+ public async Task<ActionResult> PostCapabilities(
[FromQuery] string? id,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
@@ -393,7 +405,7 @@ namespace Jellyfin.Api.Controllers
{
if (string.IsNullOrWhiteSpace(id))
{
- id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
}
_sessionManager.ReportCapabilities(id, new ClientCapabilities
@@ -417,13 +429,13 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Capabilities/Full")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostFullCapabilities(
+ public async Task<ActionResult> PostFullCapabilities(
[FromQuery] string? id,
[FromBody, Required] ClientCapabilitiesDto capabilities)
{
if (string.IsNullOrWhiteSpace(id))
{
- id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
}
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
@@ -441,11 +453,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Viewing")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult ReportViewing(
+ public async Task<ActionResult> ReportViewing(
[FromQuery] string? sessionId,
[FromQuery, Required] string? itemId)
{
- string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
_sessionManager.ReportNowViewingItem(session, itemId);
return NoContent();
@@ -459,11 +471,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Logout")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult ReportSessionEnded()
+ public async Task<ActionResult> ReportSessionEnded()
{
- AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
+ AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
- _sessionManager.Logout(auth.Token);
+ await _sessionManager.Logout(auth.Token).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index a01a617fc..c49bde93f 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
NetworkConfiguration settings = _config.GetNetworkConfiguration();
settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
- _config.SaveConfiguration("network", settings);
+ _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index da8f8b199..4422ef32c 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index b473574e0..16acedcf3 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
{
var video = (Video)_libraryManager.GetItemById(itemId);
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
+ return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
long positionTicks = 0;
- var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
+ var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
while (positionTicks < runtime)
{
@@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
var url = string.Format(
- CultureInfo.CurrentCulture,
+ CultureInfo.InvariantCulture,
"stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture),
@@ -417,6 +417,8 @@ namespace Jellyfin.Api.Controllers
IsForced = body.IsForced,
Stream = memoryStream
}).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+
return NoContent();
}
@@ -526,7 +528,7 @@ namespace Jellyfin.Api.Controllers
if (fontFile != null && fileSize != null && fileSize > 0)
{
- _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
+ _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize);
return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
}
else
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index a811a29c3..af77c801f 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
@@ -59,7 +58,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index f878f2329..c6b70f3d2 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.SyncPlayDtos;
@@ -51,10 +52,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
- public ActionResult SyncPlayCreateGroup(
+ public async Task<ActionResult> SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -69,10 +70,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
- public ActionResult SyncPlayJoinGroup(
+ public async Task<ActionResult> SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
_syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -86,9 +87,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Leave")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayLeaveGroup()
+ public async Task<ActionResult> SyncPlayLeaveGroup()
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new LeaveGroupRequest();
_syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
- public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
+ public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new ListGroupsRequest();
return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest));
}
@@ -118,10 +119,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlaySetNewQueue(
+ public async Task<ActionResult> SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PlayGroupRequest(
requestData.PlayingQueue,
requestData.PlayingItemPosition,
@@ -139,10 +140,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlaySetPlaylistItem(
+ public async Task<ActionResult> SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -157,11 +158,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayRemoveFromPlaylist(
+ public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -175,10 +176,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayMovePlaylistItem(
+ public async Task<ActionResult> SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -193,10 +194,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayQueue(
+ public async Task<ActionResult> SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -210,9 +211,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayUnpause()
+ public async Task<ActionResult> SyncPlayUnpause()
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new UnpauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -226,9 +227,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayPause()
+ public async Task<ActionResult> SyncPlayPause()
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -242,9 +243,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayStop()
+ public async Task<ActionResult> SyncPlayStop()
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new StopGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -259,10 +260,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlaySeek(
+ public async Task<ActionResult> SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -277,10 +278,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayBuffering(
+ public async Task<ActionResult> SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new BufferGroupRequest(
requestData.When,
requestData.PositionTicks,
@@ -299,10 +300,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayReady(
+ public async Task<ActionResult> SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new ReadyGroupRequest(
requestData.When,
requestData.PositionTicks,
@@ -321,10 +322,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlaySetIgnoreWait(
+ public async Task<ActionResult> SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -339,10 +340,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayNextItem(
+ public async Task<ActionResult> SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -357,10 +358,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlayPreviousItem(
+ public async Task<ActionResult> SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -375,10 +376,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlaySetRepeatMode(
+ public async Task<ActionResult> SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -393,10 +394,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public ActionResult SyncPlaySetShuffleMode(
+ public async Task<ActionResult> SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -410,10 +411,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayPing(
+ public async Task<ActionResult> SyncPlayPing(
[FromBody, Required] PingRequestDto requestData)
{
- var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
var syncPlayRequest = new PingGroupRequest(requestData.Ping);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index bbbe5fb8d..411c987f3 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -66,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo()
{
- return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
+ return _appHost.GetSystemInfo(Request);
}
/// <summary>
@@ -78,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{
- return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
+ return _appHost.GetPublicSystemInfo(Request);
}
/// <summary>
@@ -201,7 +200,7 @@ namespace Jellyfin.Api.Controllers
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
- FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
+ FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
return File(stream, "text/plain; charset=utf-8");
}
@@ -212,10 +211,13 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
[HttpGet("WakeOnLanInfo")]
[Authorize(Policy = Policies.DefaultAuthorization)]
+ [Obsolete("This endpoint is obsolete.")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
{
- var result = _appHost.GetWakeOnLanInfo();
+ var result = _network.GetMacAddresses()
+ .Select(i => new WakeOnLanInfo(i))
+ .ToList();
return Ok(result);
}
}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index 7df51c7af..e7c5a7125 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -21,10 +21,10 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UtcTimeResponse> GetUtcTime()
{
// Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
+ var requestReceptionTime = DateTime.UtcNow;
// Important to keep the following line at the end
- var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
+ var responseTransmissionTime = DateTime.UtcNow;
// Implementing NTP on such a high level results in this useless
// information being sent. On the other hand it enables future additions.
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index ffb726fab..e20bcd7a7 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -6,7 +6,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="seriesId">Optional. Filter by series id.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="enableImges">Optional. Include image information in output.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? seriesId,
[FromQuery] Guid? parentId,
- [FromQuery] bool? enableImges,
+ [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="enableImges">Optional. Include image information in output.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] Guid? parentId,
- [FromQuery] bool? enableImges,
+ [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
@@ -147,17 +147,17 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
+ var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
var parentIdGuid = parentId ?? Guid.Empty;
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
MinPremiereDate = minPremiereDate,
StartIndex = startIndex,
@@ -223,12 +223,12 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
var item = _libraryManager.GetItemById(seasonId.Value);
- if (!(item is Season seasonItem))
+ if (item is not Season seasonItem)
{
return NotFound("No season exists with Id " + seasonId);
}
@@ -237,7 +237,7 @@ namespace Jellyfin.Api.Controllers
}
else if (season.HasValue) // Season number was supplied. Get episodes by season number
{
- if (!(_libraryManager.GetItemById(seriesId) is Series series))
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
{
return NotFound("Series not found");
}
@@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers
}
else // No season number or season id was supplied. Returning all episodes.
{
- if (!(_libraryManager.GetItemById(seriesId) is Series series))
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
{
return NotFound("Series not found");
}
@@ -336,7 +336,7 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- if (!(_libraryManager.GetItemById(seriesId) is Series series))
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
{
return NotFound("Series not found");
}
@@ -350,7 +350,7 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 679f055bc..bc9527a0b 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
- _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
+ (await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
- var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
+ var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
null,
null,
maxAudioChannels,
- info!.PlaySessionId!,
+ info.PlaySessionId!,
userId ?? Guid.Empty,
true,
true,
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index b13db4baa..4263d4fe5 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
@@ -38,6 +39,7 @@ namespace Jellyfin.Api.Controllers
private readonly IAuthorizationContext _authContext;
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
+ private readonly IQuickConnect _quickConnectManager;
/// <summary>
/// Initializes a new instance of the <see cref="UserController"/> class.
@@ -49,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
public UserController(
IUserManager userManager,
ISessionManager sessionManager,
@@ -56,7 +59,8 @@ namespace Jellyfin.Api.Controllers
IDeviceManager deviceManager,
IAuthorizationContext authContext,
IServerConfigurationManager config,
- ILogger<UserController> logger)
+ ILogger<UserController> logger,
+ IQuickConnect quickConnectManager)
{
_userManager = userManager;
_sessionManager = sessionManager;
@@ -65,6 +69,7 @@ namespace Jellyfin.Api.Controllers
_authContext = authContext;
_config = config;
_logger = logger;
+ _quickConnectManager = quickConnectManager;
}
/// <summary>
@@ -77,11 +82,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<UserDto>> GetUsers(
+ public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers(
[FromQuery] bool? isHidden,
[FromQuery] bool? isDisabled)
{
- var users = Get(isHidden, isDisabled, false, false);
+ var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false);
return Ok(users);
}
@@ -92,15 +97,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
[HttpGet("Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
+ public async Task<ActionResult<IEnumerable<UserDto>>> GetPublicUsers()
{
// If the startup wizard hasn't been completed then just return all users
if (!_config.Configuration.IsStartupWizardCompleted)
{
- return Ok(Get(false, false, false, false));
+ return Ok(await Get(false, false, false, false).ConfigureAwait(false));
}
- return Ok(Get(false, false, true, true));
+ return Ok(await Get(false, false, true, true).ConfigureAwait(false));
}
/// <summary>
@@ -141,7 +146,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
- _sessionManager.RevokeUserTokens(user.Id, null);
+ await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
return NoContent();
}
@@ -195,7 +200,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
{
- var auth = _authContext.GetAuthorizationInfo(Request);
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
try
{
@@ -228,23 +233,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
[HttpPost("AuthenticateWithQuickConnect")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
{
- var auth = _authContext.GetAuthorizationInfo(Request);
-
try
{
- var authRequest = new AuthenticationRequest
- {
- App = auth.Client,
- AppVersion = auth.Version,
- DeviceId = auth.DeviceId,
- DeviceName = auth.Device,
- };
-
- return await _sessionManager.AuthenticateQuickConnect(
- authRequest,
- request.Token).ConfigureAwait(false);
+ return _quickConnectManager.GetAuthorizedRequest(request.Secret);
}
catch (SecurityException e)
{
@@ -271,7 +264,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserPassword request)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
@@ -303,9 +296,9 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
- var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
+ var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
- _sessionManager.RevokeUserTokens(user.Id, currentToken);
+ await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
return NoContent();
@@ -325,11 +318,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateUserEasyPassword(
+ public async Task<ActionResult> UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserEasyPassword request)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
@@ -343,11 +336,11 @@ namespace Jellyfin.Api.Controllers
if (request.ResetPassword)
{
- _userManager.ResetEasyPassword(user);
+ await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
}
else
{
- _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
+ await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
}
return NoContent();
@@ -371,7 +364,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UserDto updateUser)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
@@ -431,8 +424,8 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
- var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
- _sessionManager.RevokeUserTokens(user.Id, currentToken);
+ var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
+ await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
@@ -456,7 +449,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UserConfiguration userConfig)
{
- if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
+ if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
@@ -555,7 +548,7 @@ namespace Jellyfin.Api.Controllers
return _userManager.GetUserDto(user);
}
- private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
+ private async Task<IEnumerable<UserDto>> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;
@@ -571,7 +564,7 @@ namespace Jellyfin.Api.Controllers
if (filterByDevice)
{
- var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
+ var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId))
{
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 1d70406ac..f6fbdc302 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -6,10 +6,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -213,7 +212,7 @@ namespace Jellyfin.Api.Controllers
if (item is IHasTrailers hasTrailers)
{
- var trailers = hasTrailers.GetTrailers();
+ var trailers = hasTrailers.LocalTrailers;
var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
dtosExtras.CopyTo(allTrailers, 0);
@@ -297,7 +296,7 @@ namespace Jellyfin.Api.Controllers
new LatestItemsQuery
{
GroupItems = groupItems,
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ IncludeItemTypes = includeItemTypes,
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId ?? Guid.Empty,
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 7bc5ecdf1..3d27371f6 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
[HttpGet("Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
@@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers
query.PresetViews = presetViews;
}
- var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
+ var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty;
if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
{
query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 34a1d1842..bdd2612d1 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -20,12 +19,10 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
@@ -140,7 +137,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -267,6 +264,9 @@ namespace Jellyfin.Api.Controllers
// CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
+ // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
+ // since it gets disposed when ffmpeg exits
+ var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
Request,
@@ -281,7 +281,7 @@ namespace Jellyfin.Api.Controllers
_deviceManager,
_transcodingJobHelper,
TranscodingJobType,
- cancellationTokenSource.Token)
+ cancellationToken)
.ConfigureAwait(false);
TranscodingJobDto? job = null;
@@ -290,7 +290,7 @@ namespace Jellyfin.Api.Controllers
if (!System.IO.File.Exists(playlistPath))
{
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!System.IO.File.Exists(playlistPath))
@@ -317,7 +317,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments;
if (minSegments > 0)
{
- await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -485,7 +485,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
return args;
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index dc64a0f1b..3c079a71d 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -25,14 +25,12 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers
{
@@ -296,6 +294,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
@@ -308,7 +308,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -352,6 +352,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
@@ -407,6 +409,8 @@ namespace Jellyfin.Api.Controllers
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
@@ -447,22 +451,23 @@ namespace Jellyfin.Api.Controllers
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
{
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
- await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo == null)
+ {
+ return NotFound();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ return File(liveStream, MimeTypes.GetMimeType("file.ts"));
}
// Static remote stream
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
{
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
@@ -479,7 +484,7 @@ namespace Jellyfin.Api.Controllers
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
// Static stream
if (@static.HasValue && @static.Value)
@@ -488,13 +493,8 @@ namespace Jellyfin.Api.Controllers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return File(Response.Body, contentType);
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return File(liveStream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
@@ -550,6 +550,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
@@ -562,7 +564,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
@@ -606,6 +608,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
@@ -657,6 +661,8 @@ namespace Jellyfin.Api.Controllers
startTimeTicks,
width,
height,
+ maxWidth,
+ maxHeight,
videoBitRate,
subtitleStreamIndex,
subtitleMethod,
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index d6dc6650c..8be6fd1b5 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -8,6 +8,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;
@@ -101,8 +102,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
- IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypes,
DtoOptions = dtoOptions
};
@@ -209,7 +210,7 @@ namespace Jellyfin.Api.Controllers
}
// Include MediaTypes
- if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return false;
}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 06173315a..5e338b67d 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using Jellyfin.Api.Helpers;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs
index 264131905..bec961dad 100644
--- a/Jellyfin.Api/Helpers/AudioHelper.cs
+++ b/Jellyfin.Api/Helpers/AudioHelper.cs
@@ -1,4 +1,5 @@
-using System.Net.Http;
+using System.IO;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
@@ -11,12 +12,10 @@ using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Helpers
{
@@ -122,14 +121,15 @@ namespace Jellyfin.Api.Helpers
{
StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager);
- await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo == null)
+ {
+ throw new FileNotFoundException();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts"));
}
// Static remote stream
@@ -147,7 +147,7 @@ namespace Jellyfin.Api.Helpers
}
var outputPath = state.OutputFilePath;
- var outputPathExists = System.IO.File.Exists(outputPath);
+ var outputPathExists = File.Exists(outputPath);
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
var isTranscodeCached = outputPathExists && transcodingJob != null;
@@ -161,13 +161,8 @@ namespace Jellyfin.Api.Helpers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType);
+ var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
index 29e6b4193..c1c2f93b4 100644
--- a/Jellyfin.Api/Helpers/ClaimHelpers.cs
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Helpers
var value = GetClaimValue(user, InternalClaimTypes.UserId);
return string.IsNullOrEmpty(value)
? null
- : (Guid?)Guid.Parse(value);
+ : Guid.Parse(value);
}
/// <summary>
diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
deleted file mode 100644
index a911a3324..000000000
--- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Reflection;
-
-namespace Jellyfin.Api.Helpers
-{
- /// <summary>
- /// A static class for copying matching properties from one object to another.
- /// TODO: remove at the point when a fixed migration path has been decided upon.
- /// </summary>
- public static class ClassMigrationHelper
- {
- /// <summary>
- /// Extension for 'Object' that copies the properties to a destination object.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="destination">The destination.</param>
- public static void CopyProperties(this object source, object destination)
- {
- // If any this null throw an exception.
- if (source == null || destination == null)
- {
- throw new Exception("Source or/and Destination Objects are null");
- }
-
- // Getting the Types of the objects.
- Type typeDest = destination.GetType();
- Type typeSrc = source.GetType();
-
- // Iterate the Properties of the source instance and populate them from their destination counterparts.
- PropertyInfo[] srcProps = typeSrc.GetProperties();
- foreach (PropertyInfo srcProp in srcProps)
- {
- if (!srcProp.CanRead)
- {
- continue;
- }
-
- var targetProperty = typeDest.GetProperty(srcProp.Name);
- if (targetProperty == null)
- {
- continue;
- }
-
- if (!targetProperty.CanWrite)
- {
- continue;
- }
-
- var obj = targetProperty.GetSetMethod(true);
- if (obj != null && obj.IsPrivate)
- {
- continue;
- }
-
- var target = targetProperty.GetSetMethod();
- if (target != null && (target.Attributes & MethodAttributes.Static) != 0)
- {
- continue;
- }
-
- if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType))
- {
- continue;
- }
-
- // Passed all tests, lets set the value.
- targetProperty.SetValue(destination, srcProp.GetValue(source, null), null);
- }
- }
- }
-}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index dc5d6715b..02af2e435 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -18,11 +18,9 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -464,6 +462,11 @@ namespace Jellyfin.Api.Helpers
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
{
+ if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
+ {
+ return;
+ }
+
var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index b0fd59e5e..6385b62c9 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -40,7 +41,7 @@ namespace Jellyfin.Api.Helpers
// Can't dispose the response as it's required up the call chain.
var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
- var contentType = response.Content.Headers.ContentType?.ToString();
+ var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs
index d0666034e..456762147 100644
--- a/Jellyfin.Api/Helpers/HlsHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsHelpers.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
@@ -39,7 +38,7 @@ namespace Jellyfin.Api.Helpers
FileAccess.Read,
FileShare.ReadWrite,
IODefaults.FileStreamBufferSize,
- FileOptions.SequentialScan);
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
using var reader = new StreamReader(fileStream);
@@ -99,8 +98,7 @@ namespace Jellyfin.Api.Helpers
return fmp4InitFileName;
}
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- if (isWindows)
+ if (OperatingSystem.IsWindows())
{
// on Windows
// #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 295cfaf08..3b8dc7e31 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -468,7 +468,7 @@ namespace Jellyfin.Api.Helpers
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
{
- var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
+ var authInfo = await _authContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
deleted file mode 100644
index 963e17724..000000000
--- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using System;
-using System.Buffers;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using Jellyfin.Api.Models.PlaybackDtos;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
-
-namespace Jellyfin.Api.Helpers
-{
- /// <summary>
- /// Progressive file copier.
- /// </summary>
- public class ProgressiveFileCopier
- {
- private readonly TranscodingJobDto? _job;
- private readonly string? _path;
- private readonly CancellationToken _cancellationToken;
- private readonly IDirectStreamProvider? _directStreamProvider;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private long _bytesWritten;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
- /// </summary>
- /// <param name="path">The path to copy from.</param>
- /// <param name="job">The transcoding job.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
- {
- _path = path;
- _job = job;
- _cancellationToken = cancellationToken;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
- /// </summary>
- /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
- /// <param name="job">The transcoding job.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
- {
- _directStreamProvider = directStreamProvider;
- _job = job;
- _cancellationToken = cancellationToken;
- _transcodingJobHelper = transcodingJobHelper;
- }
-
- /// <summary>
- /// Gets or sets a value indicating whether allow read end of file.
- /// </summary>
- public bool AllowEndOfFile { get; set; } = true;
-
- /// <summary>
- /// Gets or sets copy start position.
- /// </summary>
- public long StartPosition { get; set; }
-
- /// <summary>
- /// Write source stream to output.
- /// </summary>
- /// <param name="outputStream">Output stream.</param>
- /// <param name="cancellationToken">Cancellation token.</param>
- /// <returns>A <see cref="Task"/>.</returns>
- public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
- {
- using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken);
- cancellationToken = linkedCancellationTokenSource.Token;
-
- try
- {
- if (_directStreamProvider != null)
- {
- await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
- return;
- }
-
- var fileOptions = FileOptions.SequentialScan;
- var allowAsyncFileRead = false;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- fileOptions |= FileOptions.Asynchronous;
- allowAsyncFileRead = true;
- }
-
- if (_path == null)
- {
- throw new ResourceNotFoundException(nameof(_path));
- }
-
- await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
-
- var eofCount = 0;
- const int EmptyReadLimit = 20;
- if (StartPosition > 0)
- {
- inputStream.Position = StartPosition;
- }
-
- while (eofCount < EmptyReadLimit || !AllowEndOfFile)
- {
- var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
-
- if (bytesRead == 0)
- {
- if (_job == null || _job.HasExited)
- {
- eofCount++;
- }
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- eofCount = 0;
- }
- }
- }
- finally
- {
- if (_job != null)
- {
- _transcodingJobHelper.OnTranscodeEndRequest(_job);
- }
- }
- }
-
- private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
- {
- var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
- try
- {
- int bytesRead;
- int totalBytesRead = 0;
-
- if (readAsync)
- {
- bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = source.Read(array, 0, array.Length);
- }
-
- while (bytesRead != 0)
- {
- var bytesToWrite = bytesRead;
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
-
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
- }
-
- if (readAsync)
- {
- bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = source.Read(array, 0, array.Length);
- }
- }
-
- return totalBytesRead;
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(array);
- }
- }
- }
-}
diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index 824870c7e..3fa07720a 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -1,6 +1,6 @@
using System;
+using System.Diagnostics;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -13,11 +13,10 @@ namespace Jellyfin.Api.Helpers
/// </summary>
public class ProgressiveFileStream : Stream
{
- private readonly FileStream _fileStream;
+ private readonly Stream _stream;
private readonly TranscodingJobDto? _job;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly bool _allowAsyncFileRead;
- private int _bytesWritten;
+ private readonly TranscodingJobHelper? _transcodingJobHelper;
+ private readonly int _timeoutMs;
private bool _disposed;
/// <summary>
@@ -26,27 +25,31 @@ namespace Jellyfin.Api.Helpers
/// <param name="filePath">The path to the transcoded file.</param>
/// <param name="job">The transcoding job information.</param>
/// <param name="transcodingJobHelper">The transcoding job helper.</param>
- public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper)
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
{
_job = job;
_transcodingJobHelper = transcodingJobHelper;
- _bytesWritten = 0;
+ _timeoutMs = timeoutMs;
- var fileOptions = FileOptions.SequentialScan;
- _allowAsyncFileRead = false;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- fileOptions |= FileOptions.Asynchronous;
- _allowAsyncFileRead = true;
- }
+ _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
+ }
- _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
+ /// </summary>
+ /// <param name="stream">The stream to progressively copy.</param>
+ /// <param name="timeoutMs">The timeout duration in milliseconds.</param>
+ public ProgressiveFileStream(Stream stream, int timeoutMs = 30000)
+ {
+ _job = null;
+ _transcodingJobHelper = null;
+ _timeoutMs = timeoutMs;
+ _stream = stream;
}
/// <inheritdoc />
- public override bool CanRead => _fileStream.CanRead;
+ public override bool CanRead => _stream.CanRead;
/// <inheritdoc />
public override bool CanSeek => false;
@@ -67,60 +70,58 @@ namespace Jellyfin.Api.Helpers
/// <inheritdoc />
public override void Flush()
{
- _fileStream.Flush();
+ // Not supported
}
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
- {
- return _fileStream.Read(buffer, offset, count);
- }
+ => Read(buffer.AsSpan(offset, count));
/// <inheritdoc />
- public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ public override int Read(Span<byte> buffer)
{
int totalBytesRead = 0;
- int remainingBytesToRead = count;
+ var stopwatch = Stopwatch.StartNew();
- int newOffset = offset;
- while (remainingBytesToRead > 0)
+ while (KeepReading(stopwatch.ElapsedMilliseconds))
{
- cancellationToken.ThrowIfCancellationRequested();
- int bytesRead;
- if (_allowAsyncFileRead)
+ totalBytesRead += _stream.Read(buffer);
+ if (totalBytesRead > 0)
{
- bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
+ break;
}
- remainingBytesToRead -= bytesRead;
- newOffset += bytesRead;
+ Thread.Sleep(50);
+ }
- if (bytesRead > 0)
- {
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
+ UpdateBytesWritten(totalBytesRead);
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
- }
- else
- {
- // If the job is null it's a live stream and will require user action to close
- if (_job?.HasExited ?? false)
- {
- break;
- }
+ return totalBytesRead;
+ }
+
+ /// <inheritdoc />
+ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
+ /// <inheritdoc />
+ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ int totalBytesRead = 0;
+ var stopwatch = Stopwatch.StartNew();
+
+ while (KeepReading(stopwatch.ElapsedMilliseconds))
+ {
+ totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+ if (totalBytesRead > 0)
+ {
+ break;
}
+
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
+ UpdateBytesWritten(totalBytesRead);
+
return totalBytesRead;
}
@@ -148,11 +149,11 @@ namespace Jellyfin.Api.Helpers
{
if (disposing)
{
- _fileStream.Dispose();
+ _stream.Dispose();
if (_job != null)
{
- _transcodingJobHelper.OnTranscodeEndRequest(_job);
+ _transcodingJobHelper?.OnTranscodeEndRequest(_job);
}
}
}
@@ -162,5 +163,19 @@ namespace Jellyfin.Api.Helpers
base.Dispose(disposing);
}
}
+
+ private void UpdateBytesWritten(int totalBytesRead)
+ {
+ if (_job != null)
+ {
+ _job.BytesDownloaded += totalBytesRead;
+ }
+ }
+
+ private bool KeepReading(long elapsed)
+ {
+ // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely
+ return !_job?.HasExited ?? elapsed < _timeoutMs;
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 56585aeab..ca8bc0bdd 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
@@ -59,9 +60,9 @@ namespace Jellyfin.Api.Helpers
/// <param name="userId">The user id.</param>
/// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
/// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
- internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
+ internal static async Task<bool> AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
{
- var auth = authContext.GetAuthorizationInfo(requestContext);
+ var auth = await authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var authenticatedUser = auth.User;
@@ -75,17 +76,17 @@ namespace Jellyfin.Api.Helpers
return true;
}
- internal static SessionInfo GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
+ internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
{
- var authorization = authContext.GetAuthorizationInfo(request);
+ var authorization = await authContext.GetAuthorizationInfo(request).ConfigureAwait(false);
var user = authorization.User;
- var session = sessionManager.LogSessionActivity(
+ var session = await sessionManager.LogSessionActivity(
authorization.Client,
authorization.Version,
authorization.DeviceId,
authorization.Device,
request.HttpContext.GetNormalizedRemoteIp().ToString(),
- user);
+ user).ConfigureAwait(false);
if (session == null)
{
@@ -95,6 +96,13 @@ namespace Jellyfin.Api.Helpers
return session;
}
+ internal static async Task<string> GetSessionId(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
+ {
+ var session = await GetSession(sessionManager, authContext, request).ConfigureAwait(false);
+
+ return session.Id;
+ }
+
internal static QueryResult<BaseItemDto> CreateQueryResult(
QueryResult<(BaseItem, ItemCounts)> result,
DtoOptions dtoOptions,
@@ -129,21 +137,5 @@ namespace Jellyfin.Api.Helpers
TotalRecordCount = result.TotalRecordCount
};
}
-
- internal static string[] GetItemTypeStrings(IReadOnlyList<BaseItemKind> itemKinds)
- {
- if (itemKinds.Count == 0)
- {
- return Array.Empty<string>();
- }
-
- var itemTypes = new string[itemKinds.Count];
- for (var i = 0; i < itemKinds.Count; i++)
- {
- itemTypes[i] = itemKinds[i].ToString();
- }
-
- return itemTypes;
- }
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 8cffe9c4c..ed071bcd7 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@@ -17,9 +18,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
@@ -83,7 +82,7 @@ namespace Jellyfin.Api.Helpers
throw new ResourceNotFoundException(nameof(httpRequest.Path));
}
- var url = httpRequest.Path.Value.Split('.')[^1];
+ var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
{
@@ -91,6 +90,7 @@ namespace Jellyfin.Api.Helpers
}
var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) ||
+ streamingRequest.StreamOptions.ContainsKey("dlnaheaders") ||
string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase);
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper)
@@ -101,7 +101,7 @@ namespace Jellyfin.Api.Helpers
EnableDlnaHeaders = enableDlnaHeaders
};
- var auth = authorizationContext.GetAuthorizationInfo(httpRequest);
+ var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);
if (!auth.UserId.Equals(Guid.Empty))
{
state.User = userManager.GetUserById(auth.UserId);
@@ -149,7 +149,7 @@ namespace Jellyfin.Api.Helpers
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture));
+ : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id)
{
@@ -222,11 +222,7 @@ namespace Jellyfin.Api.Helpers
{
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream?.BitRate,
- state.VideoStream?.Width,
- state.VideoStream?.Height,
state.OutputVideoBitrate.Value,
- state.VideoStream?.Codec,
- state.OutputVideoCodec,
state.VideoRequest.MaxWidth,
state.VideoRequest.MaxHeight);
@@ -439,7 +435,9 @@ namespace Jellyfin.Api.Helpers
return ".ogv";
}
- if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
{
return ".webm";
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index c295af7eb..9d80070eb 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
@@ -86,8 +87,8 @@ namespace Jellyfin.Api.Helpers
DeleteEncodedMediaCache();
- sessionManager!.PlaybackProgress += OnPlaybackProgress;
- sessionManager!.PlaybackStart += OnPlaybackProgress;
+ sessionManager.PlaybackProgress += OnPlaybackProgress;
+ sessionManager.PlaybackStart += OnPlaybackProgress;
}
/// <summary>
@@ -282,6 +283,7 @@ namespace Jellyfin.Api.Helpers
lock (job.ProcessLock!)
{
+ #pragma warning disable CA1849 // Can't await in lock block
job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
var process = job.Process;
@@ -307,6 +309,7 @@ namespace Jellyfin.Api.Helpers
{
}
}
+ #pragma warning restore CA1849
}
if (delete(job.Path!))
@@ -380,7 +383,7 @@ namespace Jellyfin.Api.Helpers
private void DeleteHlsPartialStreamFiles(string outputFilePath)
{
var directory = Path.GetDirectoryName(outputFilePath)
- ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
var name = Path.GetFileNameWithoutExtension(outputFilePath);
@@ -444,6 +447,10 @@ namespace Jellyfin.Api.Helpers
{
var audioCodec = state.ActualOutputAudioCodec;
var videoCodec = state.ActualOutputVideoCodec;
+ var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
+ HardwareEncodingType? hardwareAccelerationType = string.IsNullOrEmpty(hardwareAccelerationTypeString)
+ ? null
+ : (HardwareEncodingType)Enum.Parse(typeof(HardwareEncodingType), hardwareAccelerationTypeString, true);
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
{
@@ -458,6 +465,7 @@ namespace Jellyfin.Api.Helpers
AudioChannels = state.OutputAudioChannels,
IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
+ HardwareAccelerationType = hardwareAccelerationType,
TranscodeReasons = state.TranscodeReasons
});
}
@@ -490,7 +498,7 @@ namespace Jellyfin.Api.Helpers
if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
- var auth = _authorizationContext.GetAuthorizationInfo(request);
+ var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
@@ -535,8 +543,7 @@ namespace Jellyfin.Api.Helpers
state,
cancellationTokenSource);
- var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
- _logger.LogInformation(commandLineLogMessage);
+ _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
var logFilePrefix = "FFmpeg.Transcode-";
if (state.VideoRequest != null
@@ -552,10 +559,11 @@ namespace Jellyfin.Api.Helpers
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
// FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
- Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
- await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
+ await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
@@ -602,6 +610,10 @@ namespace Jellyfin.Api.Helpers
{
StartThrottler(state, transcodingJob);
}
+ else if (transcodingJob.ExitCode != 0)
+ {
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
+ }
_logger.LogDebug("StartFfMpeg() finished successfully");
@@ -738,6 +750,7 @@ namespace Jellyfin.Api.Helpers
private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
{
job.HasExited = true;
+ job.ExitCode = process.ExitCode;
_logger.LogDebug("Disposing stream resources");
state.Dispose();
@@ -759,8 +772,8 @@ namespace Jellyfin.Api.Helpers
if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
{
var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
- new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
- cancellationTokenSource.Token)
+ new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
+ cancellationTokenSource.Token)
.ConfigureAwait(false);
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
@@ -873,8 +886,8 @@ namespace Jellyfin.Api.Helpers
if (disposing)
{
_loggerFactory.Dispose();
- _sessionManager!.PlaybackProgress -= OnPlaybackProgress;
- _sessionManager!.PlaybackStart -= OnPlaybackProgress;
+ _sessionManager.PlaybackProgress -= OnPlaybackProgress;
+ _sessionManager.PlaybackStart -= OnPlaybackProgress;
}
}
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index bd7da9b06..ccd647ebe 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,19 +6,17 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.7" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.4" />
- <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.1.4" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
+ <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" />
</ItemGroup>
<ItemGroup>
@@ -33,10 +31,6 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Jellyfin.Api.Tests</_Parameter1>
diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
index be2045fba..d2e78ac88 100644
--- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
@@ -32,7 +32,8 @@ namespace Jellyfin.Api.ModelBinders
{
try
{
- var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue);
+ // REVIEW: This shouldn't be null here
+ var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue!);
bindingContext.Result = ModelBindingResult.Success(convertedValue);
}
catch (FormatException e)
@@ -44,4 +45,4 @@ namespace Jellyfin.Api.ModelBinders
return Task.CompletedTask;
}
}
-} \ No newline at end of file
+}
diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs
new file mode 100644
index 000000000..44509a9c0
--- /dev/null
+++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs
@@ -0,0 +1,22 @@
+namespace Jellyfin.Api.Models.ClientLogDtos
+{
+ /// <summary>
+ /// Client log document response dto.
+ /// </summary>
+ public class ClientLogDocumentResponseDto
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class.
+ /// </summary>
+ /// <param name="fileName">The file name.</param>
+ public ClientLogDocumentResponseDto(string fileName)
+ {
+ FileName = fileName;
+ }
+
+ /// <summary>
+ /// Gets the resulting filename.
+ /// </summary>
+ public string FileName { get; }
+ }
+}
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 8913180e4..411e4c550 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index 291e571dc..ab67c8732 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -107,6 +107,11 @@ namespace Jellyfin.Api.Models.PlaybackDtos
public bool HasExited { get; set; }
/// <summary>
+ /// Gets or sets exit code.
+ /// </summary>
+ public int ExitCode { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether is user paused.
/// </summary>
public bool IsUserPaused { get; set; }
@@ -129,7 +134,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// <summary>
/// Gets or sets bytes downloaded.
/// </summary>
- public long? BytesDownloaded { get; set; }
+ public long BytesDownloaded { get; set; }
/// <summary>
/// Gets or sets bytes transcoded.
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index 7b32d76ba..7a1ca252c 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -141,7 +141,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds)
{
- var bytesDownloaded = job.BytesDownloaded ?? 0;
+ var bytesDownloaded = job.BytesDownloaded;
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
@@ -197,7 +197,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
}
}
- _logger.LogDebug("No throttle data for " + path);
+ _logger.LogDebug("No throttle data for {Path}", path);
return false;
}
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 65d4b644e..0761b2085 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos
{
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index e58095536..fa62472e1 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Session;
@@ -85,4 +85,4 @@ namespace Jellyfin.Api.Models.SessionDtos
};
}
}
-} \ No newline at end of file
+}
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index e95f2d1f4..cbabf087b 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -55,11 +55,14 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// <summary>
/// Gets the video request.
/// </summary>
- public VideoRequestDto? VideoRequest => Request! as VideoRequestDto;
+ public VideoRequestDto? VideoRequest => Request as VideoRequestDto;
/// <summary>
/// Gets or sets the direct stream provicer.
/// </summary>
+ /// <remarks>
+ /// Deprecated.
+ /// </remarks>
public IDirectStreamProvider? DirectStreamProvider { get; set; }
/// <summary>
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
index e9b2b2cb3..02ce5a048 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
@@ -17,9 +17,21 @@ namespace Jellyfin.Api.Models.SyncPlayDtos
}
/// <summary>
- /// Gets or sets the playlist identifiers ot the items.
+ /// Gets or sets the playlist identifiers ot the items. Ignored when clearing the playlist.
/// </summary>
/// <value>The playlist identifiers ot the items.</value>
public IReadOnlyList<Guid> PlaylistItemIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the entire playlist should be cleared.
+ /// </summary>
+ /// <value>Whether the entire playlist should be cleared.</value>
+ public bool ClearPlaylist { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the playing item should be removed as well. Used only when clearing the playlist.
+ /// </summary>
+ /// <value>Whether the playing item should be removed as well.</value>
+ public bool ClearPlayingItem { get; set; }
}
}
diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
index c3a2d5cec..9493c08c2 100644
--- a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
+++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
@@ -8,9 +8,9 @@ namespace Jellyfin.Api.Models.UserDtos
public class QuickConnectDto
{
/// <summary>
- /// Gets or sets the quick connect token.
+ /// Gets or sets the quick connect secret.
/// </summary>
[Required]
- public string? Token { get; set; }
+ public string Secret { get; set; } = null!;
}
}
diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs
new file mode 100644
index 000000000..392ef5ff4
--- /dev/null
+++ b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs
@@ -0,0 +1,23 @@
+namespace Jellyfin.Data.Dtos
+{
+ /// <summary>
+ /// A dto representing custom options for a device.
+ /// </summary>
+ public class DeviceOptionsDto
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ public int Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ public string? DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom name.
+ /// </summary>
+ public string? CustomName { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/Security/ApiKey.cs b/Jellyfin.Data/Entities/Security/ApiKey.cs
new file mode 100644
index 000000000..31d865d01
--- /dev/null
+++ b/Jellyfin.Data/Entities/Security/ApiKey.cs
@@ -0,0 +1,56 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Globalization;
+
+namespace Jellyfin.Data.Entities.Security
+{
+ /// <summary>
+ /// An entity representing an API key.
+ /// </summary>
+ public class ApiKey
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ApiKey"/> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ public ApiKey(string name)
+ {
+ Name = name;
+
+ AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ DateCreated = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date of last activity.
+ /// </summary>
+ public DateTime DateLastActivity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string AccessToken { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Data/Entities/Security/Device.cs
new file mode 100644
index 000000000..67d7f78ed
--- /dev/null
+++ b/Jellyfin.Data/Entities/Security/Device.cs
@@ -0,0 +1,107 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Globalization;
+
+namespace Jellyfin.Data.Entities.Security
+{
+ /// <summary>
+ /// An entity representing a device.
+ /// </summary>
+ public class Device
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Device"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="appName">The app name.</param>
+ /// <param name="appVersion">The app version.</param>
+ /// <param name="deviceName">The device name.</param>
+ /// <param name="deviceId">The device id.</param>
+ public Device(Guid userId, string appName, string appVersion, string deviceName, string deviceId)
+ {
+ UserId = userId;
+ AppName = appName;
+ AppVersion = appVersion;
+ DeviceName = deviceName;
+ DeviceId = deviceId;
+
+ AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ DateCreated = DateTime.UtcNow;
+ DateModified = DateCreated;
+ DateLastActivity = DateCreated;
+
+ // Non-nullable for EF Core, as this is a required relationship.
+ User = null!;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the user id.
+ /// </summary>
+ public Guid UserId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string AccessToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets the app name.
+ /// </summary>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string AppName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the app version.
+ /// </summary>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string AppVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device name.
+ /// </summary>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string DeviceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this device is active.
+ /// </summary>
+ public bool IsActive { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ public DateTime DateModified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date of last activity.
+ /// </summary>
+ public DateTime DateLastActivity { get; set; }
+
+ /// <summary>
+ /// Gets the user.
+ /// </summary>
+ public User User { get; private set; }
+ }
+}
diff --git a/Jellyfin.Data/Entities/Security/DeviceOptions.cs b/Jellyfin.Data/Entities/Security/DeviceOptions.cs
new file mode 100644
index 000000000..531f66c62
--- /dev/null
+++ b/Jellyfin.Data/Entities/Security/DeviceOptions.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities.Security
+{
+ /// <summary>
+ /// An entity representing custom options for a device.
+ /// </summary>
+ public class DeviceOptions
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceOptions"/> class.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ public DeviceOptions(string deviceId)
+ {
+ DeviceId = deviceId;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the device id.
+ /// </summary>
+ public string DeviceId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the custom name.
+ /// </summary>
+ public string? CustomName { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Enums/BaseItemKind.cs b/Jellyfin.Data/Enums/BaseItemKind.cs
index aac30279e..875781746 100644
--- a/Jellyfin.Data/Enums/BaseItemKind.cs
+++ b/Jellyfin.Data/Enums/BaseItemKind.cs
@@ -79,6 +79,16 @@
Movie,
/// <summary>
+ /// Item is a live tv channel.
+ /// </summary>
+ LiveTvChannel,
+
+ /// <summary>
+ /// Item is a live tv program.
+ /// </summary>
+ LiveTvProgram,
+
+ /// <summary>
/// Item is music album.
/// </summary>
MusicAlbum,
@@ -119,6 +129,11 @@
Playlist,
/// <summary>
+ /// Item is playlist folder.
+ /// </summary>
+ PlaylistsFolder,
+
+ /// <summary>
/// Item is program
/// </summary>
Program,
@@ -187,4 +202,4 @@
/// </summary>
Year
}
-} \ No newline at end of file
+}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 3b14d3312..87233d907 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,13 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- <Nullable>enable</Nullable>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -28,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Code analysers-->
@@ -39,7 +35,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs
index 92919d3a5..f1af099d3 100644
--- a/Jellyfin.Data/Queries/ActivityLogQuery.cs
+++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs
@@ -5,19 +5,9 @@ namespace Jellyfin.Data.Queries
/// <summary>
/// A class representing a query to the activity logs.
/// </summary>
- public class ActivityLogQuery
+ public class ActivityLogQuery : PaginatedQuery
{
/// <summary>
- /// Gets or sets the index to start at.
- /// </summary>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the maximum number of items to include.
- /// </summary>
- public int? Limit { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether to take entries with a user id.
/// </summary>
public bool? HasUserId { get; set; }
diff --git a/Jellyfin.Data/Queries/DeviceQuery.cs b/Jellyfin.Data/Queries/DeviceQuery.cs
new file mode 100644
index 000000000..083e00548
--- /dev/null
+++ b/Jellyfin.Data/Queries/DeviceQuery.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Jellyfin.Data.Queries
+{
+ /// <summary>
+ /// A query to retrieve devices.
+ /// </summary>
+ public class DeviceQuery : PaginatedQuery
+ {
+ /// <summary>
+ /// Gets or sets the user id of the device.
+ /// </summary>
+ public Guid? UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ public string? DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string? AccessToken { get; set; }
+ }
+}
diff --git a/Jellyfin.Data/Queries/PaginatedQuery.cs b/Jellyfin.Data/Queries/PaginatedQuery.cs
new file mode 100644
index 000000000..58267ebe7
--- /dev/null
+++ b/Jellyfin.Data/Queries/PaginatedQuery.cs
@@ -0,0 +1,18 @@
+namespace Jellyfin.Data.Queries
+{
+ /// <summary>
+ /// An abstract class for paginated queries.
+ /// </summary>
+ public abstract class PaginatedQuery
+ {
+ /// <summary>
+ /// Gets or sets the index to start at.
+ /// </summary>
+ public int? Skip { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of items to include.
+ /// </summary>
+ public int? Limit { get; set; }
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index ee43c2159..5fa386eca 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,13 +6,9 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -22,8 +18,8 @@
<ItemGroup>
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
- <PackageReference Include="SkiaSharp" Version="2.80.2" />
- <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
+ <PackageReference Include="SkiaSharp" Version="2.80.3" />
+ <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.3" />
</ItemGroup>
<ItemGroup>
@@ -32,11 +28,6 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
- <ItemGroup>
- <!-- Needed for https://github.com/dotnet/roslyn-analyzers/issues/4382 which is in the SDK yet -->
- <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
- </ItemGroup>
-
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 8f0fae3be..6d0a5ac2b 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using BlurHashSharp.SkiaSharp;
+using Diacritics.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging;
using SkiaSharp;
@@ -142,9 +142,6 @@ namespace Jellyfin.Drawing.Skia
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
}
- private static bool HasDiacritics(string text)
- => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
-
private bool RequiresSpecialCharacterHack(string path)
{
for (int i = 0; i < path.Length; i++)
@@ -155,7 +152,7 @@ namespace Jellyfin.Drawing.Skia
}
}
- return HasDiacritics(path);
+ return path.HasDiacritics();
}
private string NormalizePath(string path)
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 09a370238..d1cc2255d 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -112,7 +112,7 @@ namespace Jellyfin.Drawing.Skia
canvas.DrawImage(residedBackdrop, 0, 0);
// draw shadow rectangle
- var paintColor = new SKPaint
+ using var paintColor = new SKPaint
{
Color = SKColors.Black.WithAlpha(0x78),
Style = SKPaintStyle.Fill
@@ -130,7 +130,7 @@ namespace Jellyfin.Drawing.Skia
}
// draw library name
- var textPaint = new SKPaint
+ using var textPaint = new SKPaint
{
Color = SKColors.White,
Style = SKPaintStyle.Fill,
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index faf814c06..61db223d9 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -226,5 +226,10 @@ namespace Jellyfin.Networking.Configuration
/// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks.
/// </summary>
public string[] KnownProxies { get; set; } = Array.Empty<string>();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
+ /// </summary>
+ public bool EnablePublishedServerUriByRequest { get; set; } = false;
}
}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs
index ac0485d87..14726565a 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs
@@ -16,11 +16,7 @@ namespace Jellyfin.Networking.Configuration
{
return new[]
{
- new ConfigurationStore
- {
- Key = "network",
- ConfigurationType = typeof(NetworkConfiguration)
- }
+ new NetworkConfigurationStore()
};
}
}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs
new file mode 100644
index 000000000..a268ebb68
--- /dev/null
+++ b/Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Networking.Configuration
+{
+ /// <summary>
+ /// A configuration that stores network related settings.
+ /// </summary>
+ public class NetworkConfigurationStore : ConfigurationStore
+ {
+ /// <summary>
+ /// The name of the configuration in the storage.
+ /// </summary>
+ public const string StoreKey = "network";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class.
+ /// </summary>
+ public NetworkConfigurationStore()
+ {
+ ConfigurationType = typeof(NetworkConfiguration);
+ Key = StoreKey;
+ }
+ }
+}
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 63557e91f..0cd9a5915 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -1,12 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 4078fd126..58b30ad2d 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -455,10 +455,10 @@ namespace Jellyfin.Networking.Manager
}
// No bind address, so return all internal interfaces.
- return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback()));
+ return CreateCollection(_internalInterfaces);
}
- return new Collection<IPObject>(_bindAddresses);
+ return new Collection<IPObject>(_bindAddresses.Where(a => IsInLocalNetwork(a)).ToArray());
}
/// <inheritdoc/>
@@ -481,7 +481,7 @@ namespace Jellyfin.Networking.Manager
}
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
- return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address);
+ return address.IsLoopback() || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
}
/// <inheritdoc/>
@@ -647,16 +647,6 @@ namespace Jellyfin.Networking.Manager
_interfaceAddresses.AddItem(address, false);
_interfaceNames[parts[2]] = Math.Abs(index);
}
-
- if (IsIP4Enabled)
- {
- _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
- }
-
- if (IsIP6Enabled)
- {
- _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
- }
}
InitialiseLAN(config);
@@ -737,7 +727,7 @@ namespace Jellyfin.Networking.Manager
private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
{
- if (evt.Key.Equals("network", StringComparison.Ordinal))
+ if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
{
UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
}
@@ -1037,17 +1027,14 @@ namespace Jellyfin.Networking.Manager
// Subnets are the same as the calculated internal interface.
_lanSubnets = new Collection<IPObject>();
- // We must listen on loopback for LiveTV to function regardless of the settings.
if (IsIP6Enabled)
{
- _lanSubnets.AddItem(IPNetAddress.IP6Loopback);
_lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA
_lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local
}
if (IsIP4Enabled)
{
- _lanSubnets.AddItem(IPNetAddress.IP4Loopback);
_lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8"));
_lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12"));
_lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16"));
@@ -1055,17 +1042,6 @@ namespace Jellyfin.Networking.Manager
}
else
{
- // We must listen on loopback for LiveTV to function regardless of the settings.
- if (IsIP6Enabled)
- {
- _lanSubnets.AddItem(IPNetAddress.IP6Loopback);
- }
-
- if (IsIP4Enabled)
- {
- _lanSubnets.AddItem(IPNetAddress.IP4Loopback);
- }
-
// Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet.
_internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork));
}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 27360afb0..ba2c8b54f 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -62,7 +62,7 @@ namespace Jellyfin.Server.Implementations.Activity
return new QueryResult<ActivityLogEntry>
{
Items = await entries
- .Skip(query.StartIndex ?? 0)
+ .Skip(query.Skip ?? 0)
.Take(query.Limit ?? 100)
.AsAsyncEnumerable()
.Select(ConvertToOldModel)
@@ -86,15 +86,12 @@ namespace Jellyfin.Server.Implementations.Activity
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
{
- return new ActivityLogEntry
+ return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
{
Id = entry.Id,
- Name = entry.Name,
Overview = entry.Overview,
ShortOverview = entry.ShortOverview,
- Type = entry.Type,
ItemId = entry.ItemId,
- UserId = entry.UserId,
Date = entry.DateCreated,
Severity = entry.LogSeverity
};
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
new file mode 100644
index 000000000..a55949df8
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Session;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Devices
+{
+ /// <summary>
+ /// Manages the creation, updating, and retrieval of devices.
+ /// </summary>
+ public class DeviceManager : IDeviceManager
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+ private readonly IUserManager _userManager;
+ private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database provider.</param>
+ /// <param name="userManager">The user manager.</param>
+ public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
+ {
+ _dbProvider = dbProvider;
+ _userManager = userManager;
+ }
+
+ /// <inheritdoc />
+ public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>>? DeviceOptionsUpdated;
+
+ /// <inheritdoc />
+ public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
+ {
+ _capabilitiesMap[deviceId] = capabilities;
+ }
+
+ /// <inheritdoc />
+ public async Task UpdateDeviceOptions(string deviceId, string deviceName)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+ if (deviceOptions == null)
+ {
+ deviceOptions = new DeviceOptions(deviceId);
+ dbContext.DeviceOptions.Add(deviceOptions);
+ }
+
+ deviceOptions.CustomName = deviceName;
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+
+ DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
+ }
+
+ /// <inheritdoc />
+ public async Task<Device> CreateDevice(Device device)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ dbContext.Devices.Add(device);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ return device;
+ }
+
+ /// <inheritdoc />
+ public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var deviceOptions = await dbContext.DeviceOptions
+ .AsQueryable()
+ .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
+ .ConfigureAwait(false);
+
+ return deviceOptions ?? new DeviceOptions(deviceId);
+ }
+
+ /// <inheritdoc />
+ public ClientCapabilities GetCapabilities(string deviceId)
+ {
+ return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result)
+ ? result
+ : new ClientCapabilities();
+ }
+
+ /// <inheritdoc />
+ public async Task<DeviceInfo?> GetDevice(string id)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var device = await dbContext.Devices
+ .AsQueryable()
+ .Where(d => d.DeviceId == id)
+ .OrderByDescending(d => d.DateLastActivity)
+ .Include(d => d.User)
+ .FirstOrDefaultAsync()
+ .ConfigureAwait(false);
+
+ var deviceInfo = device == null ? null : ToDeviceInfo(device);
+
+ return deviceInfo;
+ }
+
+ /// <inheritdoc />
+ public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ var devices = dbContext.Devices.AsQueryable();
+
+ if (query.UserId.HasValue)
+ {
+ devices = devices.Where(device => device.UserId == query.UserId.Value);
+ }
+
+ if (query.DeviceId != null)
+ {
+ devices = devices.Where(device => device.DeviceId == query.DeviceId);
+ }
+
+ if (query.AccessToken != null)
+ {
+ devices = devices.Where(device => device.AccessToken == query.AccessToken);
+ }
+
+ var count = await devices.CountAsync().ConfigureAwait(false);
+
+ if (query.Skip.HasValue)
+ {
+ devices = devices.Skip(query.Skip.Value);
+ }
+
+ if (query.Limit.HasValue)
+ {
+ devices = devices.Take(query.Limit.Value);
+ }
+
+ return new QueryResult<Device>
+ {
+ Items = await devices.ToListAsync().ConfigureAwait(false),
+ StartIndex = query.Skip ?? 0,
+ TotalRecordCount = count
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
+ {
+ var devices = await GetDevices(query).ConfigureAwait(false);
+
+ return new QueryResult<DeviceInfo>
+ {
+ Items = devices.Items.Select(device => ToDeviceInfo(device)).ToList(),
+ StartIndex = devices.StartIndex,
+ TotalRecordCount = devices.TotalRecordCount
+ };
+ }
+
+ /// <inheritdoc />
+ public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ var sessions = dbContext.Devices
+ .Include(d => d.User)
+ .AsQueryable()
+ .OrderBy(d => d.DeviceId)
+ .ThenByDescending(d => d.DateLastActivity)
+ .AsAsyncEnumerable();
+
+ if (supportsSync.HasValue)
+ {
+ sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
+ }
+
+ if (userId.HasValue)
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
+ }
+
+ var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
+
+ return new QueryResult<DeviceInfo>(array);
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteDevice(Device device)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+ dbContext.Devices.Remove(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public bool CanAccessDevice(User user, string deviceId)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+
+ if (string.IsNullOrEmpty(deviceId))
+ {
+ throw new ArgumentNullException(nameof(deviceId));
+ }
+
+ if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
+ {
+ return true;
+ }
+
+ return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparison.OrdinalIgnoreCase)
+ || !GetCapabilities(deviceId).SupportsPersistentIdentifier;
+ }
+
+ private DeviceInfo ToDeviceInfo(Device authInfo)
+ {
+ var caps = GetCapabilities(authInfo.DeviceId);
+
+ return new DeviceInfo
+ {
+ AppName = authInfo.AppName,
+ AppVersion = authInfo.AppVersion,
+ Id = authInfo.DeviceId,
+ LastUserId = authInfo.UserId,
+ LastUserName = authInfo.User.Username,
+ Name = authInfo.DeviceName,
+ DateLastActivity = authInfo.DateLastActivity,
+ IconUrl = caps.IconUrl
+ };
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs
index 8c5d8f2ce..7f7c4750d 100644
--- a/Jellyfin.Server.Implementations/Events/EventManager.cs
+++ b/Jellyfin.Server.Implementations/Events/EventManager.cs
@@ -57,7 +57,7 @@ namespace Jellyfin.Server.Implementations.Events
}
catch (Exception e)
{
- _logger.LogError(e, "Uncaught exception in EventConsumer {type}: ", service.GetType());
+ _logger.LogError(e, "Uncaught exception in EventConsumer {Type}: ", service.GetType());
}
}
}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index eeeb1d19b..d22757c03 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,17 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- </PropertyGroup>
-
- <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code analysers-->
@@ -26,14 +18,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.7" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.7" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
+ <PackageReference Include="System.Linq.Async" Version="5.1.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.7">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs
index db648472d..dc4f53913 100644
--- a/Jellyfin.Server.Implementations/JellyfinDb.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDb.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
@@ -29,6 +30,12 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
+ public virtual DbSet<ApiKey> ApiKeys { get; set; }
+
+ public virtual DbSet<Device> Devices { get; set; }
+
+ public virtual DbSet<DeviceOptions> DeviceOptions { get; set; }
+
public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
public virtual DbSet<ImageInfo> ImageInfos { get; set; }
@@ -146,80 +153,10 @@ namespace Jellyfin.Server.Implementations
{
modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
base.OnModelCreating(modelBuilder);
-
modelBuilder.HasDefaultSchema("jellyfin");
- // Collations
-
- modelBuilder.Entity<User>()
- .Property(user => user.Username)
- .UseCollation("NOCASE");
-
- // Delete behavior
-
- modelBuilder.Entity<User>()
- .HasOne(u => u.ProfileImage)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.Permissions)
- .WithOne()
- .HasForeignKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.Preferences)
- .WithOne()
- .HasForeignKey(p => p.UserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.AccessSchedules)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.DisplayPreferences)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<User>()
- .HasMany(u => u.ItemDisplayPreferences)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- modelBuilder.Entity<DisplayPreferences>()
- .HasMany(d => d.HomeSections)
- .WithOne()
- .OnDelete(DeleteBehavior.Cascade);
-
- // Indexes
-
- modelBuilder.Entity<User>()
- .HasIndex(entity => entity.Username)
- .IsUnique();
-
- modelBuilder.Entity<DisplayPreferences>()
- .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
- .IsUnique();
-
- modelBuilder.Entity<CustomItemDisplayPreferences>()
- .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
- .IsUnique();
-
- // Used to get a user's permissions or a specific permission for a user.
- // Also prevents multiple values being created for a user.
- // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
- modelBuilder.Entity<Permission>()
- .HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
- .IsUnique();
-
- modelBuilder.Entity<Preference>()
- .HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
- .IsUnique();
+ // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDb).Assembly);
}
}
}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
index 486be6053..c2c5198d1 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs
@@ -1,8 +1,10 @@
using System;
using System.IO;
+using System.Linq;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations
{
@@ -13,19 +15,27 @@ namespace Jellyfin.Server.Implementations
{
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationPaths _appPaths;
+ private readonly ILogger<JellyfinDbProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <param name="serviceProvider">The application's service provider.</param>
/// <param name="appPaths">The application paths.</param>
- public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
+ /// <param name="logger">The logger.</param>
+ public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> logger)
{
_serviceProvider = serviceProvider;
_appPaths = appPaths;
+ _logger = logger;
using var jellyfinDb = CreateContext();
- jellyfinDb.Database.Migrate();
+ if (jellyfinDb.Database.GetPendingMigrations().Any())
+ {
+ _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
+ jellyfinDb.Database.Migrate();
+ _logger.LogInformation("EFCore migrations applied successfully");
+ }
}
/// <summary>
diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
new file mode 100644
index 000000000..7e9566e2e
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs
@@ -0,0 +1,653 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDb))]
+ [Migration("20210814002109_AddDevices")]
+ partial class AddDevices
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("jellyfin")
+ .HasAnnotation("ProductVersion", "5.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("EasyPassword")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs
new file mode 100644
index 000000000..ac062317a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs
@@ -0,0 +1,128 @@
+#pragma warning disable CS1591, SA1601
+
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ public partial class AddDevices : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "ApiKeys",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
+ DateLastActivity = table.Column<DateTime>(type: "TEXT", nullable: false),
+ Name = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
+ AccessToken = table.Column<string>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ApiKeys", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "DeviceOptions",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ DeviceId = table.Column<string>(type: "TEXT", nullable: false),
+ CustomName = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_DeviceOptions", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Devices",
+ schema: "jellyfin",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column<Guid>(type: "TEXT", nullable: false),
+ AccessToken = table.Column<string>(type: "TEXT", nullable: false),
+ AppName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
+ AppVersion = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
+ DeviceName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
+ DeviceId = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false),
+ IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
+ DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
+ DateModified = table.Column<DateTime>(type: "TEXT", nullable: false),
+ DateLastActivity = table.Column<DateTime>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Devices", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Devices_Users_UserId",
+ column: x => x.UserId,
+ principalSchema: "jellyfin",
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ApiKeys_AccessToken",
+ schema: "jellyfin",
+ table: "ApiKeys",
+ column: "AccessToken",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_DeviceOptions_DeviceId",
+ schema: "jellyfin",
+ table: "DeviceOptions",
+ column: "DeviceId",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_AccessToken_DateLastActivity",
+ schema: "jellyfin",
+ table: "Devices",
+ columns: new[] { "AccessToken", "DateLastActivity" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_DeviceId",
+ schema: "jellyfin",
+ table: "Devices",
+ column: "DeviceId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_DeviceId_DateLastActivity",
+ schema: "jellyfin",
+ table: "Devices",
+ columns: new[] { "DeviceId", "DateLastActivity" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Devices_UserId_DeviceId",
+ schema: "jellyfin",
+ table: "Devices",
+ columns: new[] { "UserId", "DeviceId" });
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "ApiKeys",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "DeviceOptions",
+ schema: "jellyfin");
+
+ migrationBuilder.DropTable(
+ name: "Devices",
+ schema: "jellyfin");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 286eb7468..fcc360e26 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
- .HasAnnotation("ProductVersion", "5.0.3");
+ .HasAnnotation("ProductVersion", "5.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -332,6 +332,114 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("Preferences");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
@@ -505,6 +613,17 @@ namespace Jellyfin.Server.Implementations.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
new file mode 100644
index 000000000..3f19b6986
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the ApiKey entity.
+ /// </summary>
+ public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ApiKey> builder)
+ {
+ builder
+ .HasIndex(entity => entity.AccessToken)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..779aec986
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the CustomItemDisplayPreferences entity.
+ /// </summary>
+ public class CustomItemDisplayPreferencesConfiguration : IEntityTypeConfiguration<CustomItemDisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<CustomItemDisplayPreferences> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs
new file mode 100644
index 000000000..a750b65c0
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs
@@ -0,0 +1,28 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Device entity.
+ /// </summary>
+ public class DeviceConfiguration : IEntityTypeConfiguration<Device>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Device> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.DeviceId });
+
+ builder
+ .HasIndex(entity => entity.DeviceId);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
new file mode 100644
index 000000000..038afd752
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Data.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DeviceOptions entity.
+ /// </summary>
+ public class DeviceOptionsConfiguration : IEntityTypeConfiguration<DeviceOptions>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DeviceOptions> builder)
+ {
+ builder
+ .HasIndex(entity => entity.DeviceId)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..9b437861b
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
@@ -0,0 +1,25 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DisplayPreferencesConfiguration entity.
+ /// </summary>
+ public class DisplayPreferencesConfiguration : IEntityTypeConfiguration<DisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DisplayPreferences> builder)
+ {
+ builder
+ .HasMany(d => d.HomeSections)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs
new file mode 100644
index 000000000..240e284c0
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs
@@ -0,0 +1,24 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PermissionConfiguration : IEntityTypeConfiguration<Permission>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Permission> builder)
+ {
+ // Used to get a user's permissions or a specific permission for a user.
+ // Also prevents multiple values being created for a user.
+ // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs
new file mode 100644
index 000000000..49c869c6a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs
@@ -0,0 +1,21 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PreferenceConfiguration : IEntityTypeConfiguration<Preference>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Preference> builder)
+ {
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs
new file mode 100644
index 000000000..a369cf656
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs
@@ -0,0 +1,56 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the User entity.
+ /// </summary>
+ public class UserConfiguration : IEntityTypeConfiguration<User>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<User> builder)
+ {
+ builder
+ .Property(user => user.Username)
+ .UseCollation("NOCASE");
+
+ builder
+ .HasOne(u => u.ProfileImage)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Permissions)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Preferences)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.AccessSchedules)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.DisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.ItemDisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => entity.Username)
+ .IsUnique();
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
new file mode 100644
index 000000000..b79e46469
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities.Security;
+using MediaBrowser.Controller.Security;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Security
+{
+ /// <inheritdoc />
+ public class AuthenticationManager : IAuthenticationManager
+ {
+ private readonly JellyfinDbProvider _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database provider.</param>
+ public AuthenticationManager(JellyfinDbProvider dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task CreateApiKey(string name)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ dbContext.ApiKeys.Add(new ApiKey(name));
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys()
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ return await dbContext.ApiKeys
+ .AsAsyncEnumerable()
+ .Select(key => new AuthenticationInfo
+ {
+ AppName = key.Name,
+ AccessToken = key.AccessToken,
+ DateCreated = key.DateCreated,
+ DeviceId = string.Empty,
+ DeviceName = string.Empty,
+ AppVersion = string.Empty
+ }).ToListAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task DeleteApiKey(string accessToken)
+ {
+ await using var dbContext = _dbProvider.CreateContext();
+
+ var key = await dbContext.ApiKeys
+ .AsQueryable()
+ .Where(apiKey => apiKey.AccessToken == accessToken)
+ .FirstOrDefaultAsync()
+ .ConfigureAwait(false);
+
+ if (key == null)
+ {
+ return;
+ }
+
+ dbContext.Remove(key);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index c87f7dbbd..d59d36e88 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -3,40 +3,40 @@
using System;
using System.Collections.Generic;
using System.Net;
-using MediaBrowser.Common.Extensions;
+using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
-namespace Emby.Server.Implementations.HttpServer.Security
+namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
- private readonly IAuthenticationRepository _authRepo;
+ private readonly JellyfinDbProvider _jellyfinDbProvider;
private readonly IUserManager _userManager;
- public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
+ public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
{
- _authRepo = authRepo;
+ _jellyfinDbProvider = jellyfinDb;
_userManager = userManager;
}
- public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
+ public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
{
- if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
+ if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
{
- return (AuthorizationInfo)cached!; // Cache should never contain null
+ return Task.FromResult((AuthorizationInfo)cached); // Cache should never contain null
}
return GetAuthorization(requestContext);
}
- public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+ public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
- var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+ var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false);
return authInfo;
}
@@ -45,22 +45,22 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private AuthorizationInfo GetAuthorization(HttpContext httpReq)
+ private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
- var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
+ var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
- private AuthorizationInfo GetAuthorizationInfoFromDictionary(
- in Dictionary<string, string>? auth,
- in IHeaderDictionary headers,
- in IQueryCollection queryString)
+ private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
+ IReadOnlyDictionary<string, string>? auth,
+ IHeaderDictionary headers,
+ IQueryCollection queryString)
{
string? deviceId = null;
- string? device = null;
+ string? deviceName = null;
string? client = null;
string? version = null;
string? token = null;
@@ -68,12 +68,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (auth != null)
{
auth.TryGetValue("DeviceId", out deviceId);
- auth.TryGetValue("Device", out device);
+ auth.TryGetValue("Device", out deviceName);
auth.TryGetValue("Client", out client);
auth.TryGetValue("Version", out version);
auth.TryGetValue("Token", out token);
}
+#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
if (string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
@@ -98,7 +99,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authInfo = new AuthorizationInfo
{
Client = client,
- Device = device,
+ Device = deviceName,
DeviceId = deviceId,
Version = version,
Token = token,
@@ -111,90 +112,95 @@ namespace Emby.Server.Implementations.HttpServer.Security
// Request doesn't contain a token.
return authInfo;
}
+#pragma warning restore CA1508
authInfo.HasToken = true;
- var result = _authRepo.Get(new AuthenticationInfoQuery
- {
- AccessToken = token
- });
+ await using var dbContext = _jellyfinDbProvider.CreateContext();
+ var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
- if (result.Items.Count > 0)
+ if (device != null)
{
authInfo.IsAuthenticated = true;
- }
-
- var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
-
- if (originalAuthenticationInfo != null)
- {
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
- authInfo.Client = originalAuthenticationInfo.AppName;
+ authInfo.Client = device.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
- authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
+ authInfo.DeviceId = device.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+ var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
- authInfo.Device = originalAuthenticationInfo.DeviceName;
+ authInfo.Device = device.DeviceName;
}
- else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- originalAuthenticationInfo.DeviceName = authInfo.Device;
+ device.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
- authInfo.Version = originalAuthenticationInfo.AppVersion;
+ authInfo.Version = device.AppVersion;
}
- else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- originalAuthenticationInfo.AppVersion = authInfo.Version;
+ device.AppVersion = authInfo.Version;
}
}
- if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
+ if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
{
- originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
+ device.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
- if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
+ authInfo.User = _userManager.GetUserById(device.UserId);
+
+ if (updateToken)
+ {
+ dbContext.Devices.Update(device);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
+ if (key != null)
{
- authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
+ authInfo.IsAuthenticated = true;
+ authInfo.Client = key.Name;
+ authInfo.Token = key.AccessToken;
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+ {
+ authInfo.DeviceId = string.Empty;
+ }
- if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
{
- originalAuthenticationInfo.UserName = authInfo.User.Username;
- updateToken = true;
+ authInfo.Device = string.Empty;
}
- authInfo.IsApiKey = false;
- }
- else
- {
- authInfo.IsApiKey = true;
- }
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
+ {
+ authInfo.Version = string.Empty;
+ }
- if (updateToken)
- {
- _authRepo.Update(originalAuthenticationInfo);
+ authInfo.IsApiKey = true;
}
}
@@ -206,7 +212,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
+ private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@@ -215,7 +221,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
- return GetAuthorization(auth.Count > 0 ? auth[0] : null);
+ return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
}
/// <summary>
@@ -223,7 +229,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
+ private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@@ -232,7 +238,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Headers[HeaderNames.Authorization];
}
- return GetAuthorization(auth.Count > 0 ? auth[0] : null);
+ return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
}
/// <summary>
@@ -240,13 +246,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
+ private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
- if (authorizationHeader == null)
- {
- return null;
- }
-
var firstSpace = authorizationHeader.IndexOf(' ');
// There should be at least two parts
@@ -263,29 +264,57 @@ namespace Emby.Server.Implementations.HttpServer.Security
return null;
}
+ // Remove up until the first space
authorizationHeader = authorizationHeader[(firstSpace + 1)..];
+ return GetParts(authorizationHeader);
+ }
- var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ /// <summary>
+ /// Get the authorization header components.
+ /// </summary>
+ /// <param name="authorizationHeader">The authorization header.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ public static Dictionary<string, string> GetParts(ReadOnlySpan<char> authorizationHeader)
+ {
+ var result = new Dictionary<string, string>();
+ var escaped = false;
+ int start = 0;
+ string key = string.Empty;
- foreach (var item in authorizationHeader.Split(','))
+ int i;
+ for (i = 0; i < authorizationHeader.Length; i++)
{
- var trimmedItem = item.Trim();
- var firstEqualsSign = trimmedItem.IndexOf('=');
-
- if (firstEqualsSign > 0)
+ var token = authorizationHeader[i];
+ if (token == '"' || token == ',')
+ {
+ // Applying a XOR logic to evaluate whether it is opening or closing a value
+ escaped = (!escaped) == (token == '"');
+ if (token == ',' && !escaped)
+ {
+ // Meeting a comma after a closing escape char means the value is complete
+ if (start < i)
+ {
+ result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
+ key = string.Empty;
+ }
+
+ start = i + 1;
+ }
+ }
+ else if (!escaped && token == '=')
{
- var key = trimmedItem[..firstEqualsSign].ToString();
- var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
- result[key] = value;
+ key = authorizationHeader[start.. i].Trim().ToString();
+ start = i + 1;
}
}
- return result;
- }
+ // Add last value
+ if (start < i)
+ {
+ result[key] = WebUtility.UrlDecode(authorizationHeader[start..i].Trim('"').ToString());
+ }
- private static string NormalizeValue(string value)
- {
- return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
+ return result;
}
}
}
diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
index 6a78e7ee6..7480a05c2 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
@@ -1,9 +1,6 @@
using System;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.Cryptography;
@@ -61,35 +58,25 @@ namespace Jellyfin.Server.Implementations.Users
}
// Handle the case when the stored password is null, but the user tried to login with a password
- if (resolvedUser.Password != null)
+ if (resolvedUser.Password == null)
{
- byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
-
- PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
- if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
- || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
- {
- byte[] calculatedHash = _cryptographyProvider.ComputeHash(
- readyHash.Id,
- passwordBytes,
- readyHash.Salt.ToArray());
-
- if (readyHash.Hash.SequenceEqual(calculatedHash))
- {
- success = true;
- }
- }
- else
- {
- throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}");
- }
+ throw new AuthenticationException("Invalid username or password");
}
+ PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
+ success = _cryptographyProvider.Verify(readyHash, password);
+
if (!success)
{
throw new AuthenticationException("Invalid username or password");
}
+ // Migrate old hashes to the new default
+ if (!string.Equals(readyHash.Id, _cryptographyProvider.DefaultHashMethod, StringComparison.Ordinal))
+ {
+ ChangePassword(resolvedUser, password);
+ }
+
return Task.FromResult(new ProviderAuthenticationResult
{
Username = username
diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index c99c5e4ef..5e84255f9 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@@ -53,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Users
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
SerializablePasswordReset spr;
- await using (var str = File.OpenRead(resetFile))
+ await using (var str = AsyncFile.OpenRead(resetFile))
{
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false)
?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid.");
@@ -92,13 +93,9 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
{
- string pin;
- using (var cryptoRandom = RandomNumberGenerator.Create())
- {
- byte[] bytes = new byte[4];
- cryptoRandom.GetBytes(bytes);
- pin = BitConverter.ToString(bytes);
- }
+ byte[] bytes = new byte[4];
+ RandomNumberGenerator.Fill(bytes);
+ string pin = BitConverter.ToString(bytes);
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
string filePath = _passwordResetFileBase + user.Id + ".json";
@@ -110,10 +107,9 @@ namespace Jellyfin.Server.Implementations.Users
UserName = user.Username
};
- await using (FileStream fileStream = File.OpenWrite(filePath))
+ await using (FileStream fileStream = AsyncFile.OpenWrite(filePath))
{
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
- await fileStream.FlushAsync().ConfigureAwait(false);
}
user.EasyPassword = pin;
@@ -122,6 +118,7 @@ namespace Jellyfin.Server.Implementations.Users
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
+ PinFile = filePath
};
}
diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
index dbba80c21..a471ea1d5 100644
--- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
+++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs
@@ -4,10 +4,10 @@ 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.Security;
using MediaBrowser.Controller.Session;
namespace Jellyfin.Server.Implementations.Users
@@ -15,14 +15,12 @@ namespace Jellyfin.Server.Implementations.Users
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
- private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
private readonly ISessionManager _sessionManager;
- public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+ public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
{
_userManager = userManager;
- _authRepo = authRepo;
_deviceManager = deviceManager;
_sessionManager = sessionManager;
}
@@ -38,27 +36,27 @@ namespace Jellyfin.Server.Implementations.Users
{
}
- private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
+ private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
{
var user = e.Argument;
if (!user.HasPermission(PermissionKind.EnableAllDevices))
{
- UpdateDeviceAccess(user);
+ await UpdateDeviceAccess(user).ConfigureAwait(false);
}
}
- private void UpdateDeviceAccess(User user)
+ private async Task UpdateDeviceAccess(User user)
{
- var existing = _authRepo.Get(new AuthenticationInfoQuery
+ var existing = (await _deviceManager.GetDevices(new DeviceQuery
{
UserId = user.Id
- }).Items;
+ }).ConfigureAwait(false)).Items;
- foreach (var authInfo in existing)
+ foreach (var device in existing)
{
- if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+ if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
{
- _sessionManager.Logout(authInfo);
+ await _sessionManager.Logout(device).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 27d4f40d3..3d0a51ff6 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -5,7 +5,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -13,7 +12,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Events.Users;
using MediaBrowser.Common;
-using MediaBrowser.Common.Cryptography;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@@ -164,15 +162,6 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void UpdateUser(User user)
- {
- using var dbContext = _dbProvider.CreateContext();
- dbContext.Users.Update(user);
- _users[user.Id] = user;
- dbContext.SaveChanges();
- }
-
- /// <inheritdoc/>
public async Task UpdateUserAsync(User user)
{
await using var dbContext = _dbProvider.CreateContext();
@@ -271,9 +260,9 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void ResetEasyPassword(User user)
+ public Task ResetEasyPassword(User user)
{
- ChangeEasyPassword(user, string.Empty, null);
+ return ChangeEasyPassword(user, string.Empty, null);
}
/// <inheritdoc/>
@@ -291,7 +280,7 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc/>
- public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
+ public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
{
if (newPassword != null)
{
@@ -304,7 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
}
user.EasyPassword = newPasswordSha1;
- UpdateUser(user);
+ await UpdateUserAsync(user).ConfigureAwait(false);
_eventManager.Publish(new UserPasswordChangedEventArgs(user));
}
@@ -539,11 +528,7 @@ namespace Jellyfin.Server.Implementations.Users
}
}
- return new PinRedeemResult
- {
- Success = false,
- UsersReset = Array.Empty<string>()
- };
+ return new PinRedeemResult();
}
/// <inheritdoc />
@@ -710,6 +695,11 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task ClearProfileImageAsync(User user)
{
+ if (user.ProfileImage == null)
+ {
+ return;
+ }
+
await using var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
@@ -826,11 +816,7 @@ namespace Jellyfin.Server.Implementations.Users
{
// Check easy password
var passwordHash = PasswordHash.Parse(user.EasyPassword);
- var hash = _cryptoProvider.ComputeHash(
- passwordHash.Id,
- Encoding.UTF8.GetBytes(password),
- passwordHash.Salt.ToArray());
- success = passwordHash.Hash.SequenceEqual(hash);
+ success = _cryptoProvider.Verify(passwordHash, password);
}
return (authenticationProvider, username, success);
diff --git a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
index 0d04b6bb1..b061be33b 100644
--- a/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
+++ b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Configuration
}
/// <inheritdoc />
- public Task<CorsPolicy> GetPolicyAsync(HttpContext context, string policyName)
+ public Task<CorsPolicy?> GetPolicyAsync(HttpContext context, string? policyName)
{
var corsHosts = _serverConfigurationManager.Configuration.CorsHosts;
var builder = new CorsPolicyBuilder()
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Configuration
.AllowCredentials();
}
- return Task.FromResult(builder.Build());
+ return Task.FromResult<CorsPolicy?>(builder.Build());
}
}
}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 94c3ca4a9..67e50b92d 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -9,16 +9,19 @@ using Jellyfin.Api.WebSocketListeners;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
+using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
+using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
+using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -38,60 +41,61 @@ namespace Jellyfin.Server
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
- IConfiguration startupConfig,
- IFileSystem fileSystem,
- IServiceCollection collection)
+ IConfiguration startupConfig)
: base(
applicationPaths,
loggerFactory,
options,
- startupConfig,
- fileSystem,
- collection)
+ startupConfig)
{
}
/// <inheritdoc/>
- protected override void RegisterServices()
+ protected override void RegisterServices(IServiceCollection serviceCollection)
{
// Register an image encoder
bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable();
Type imageEncoderType = useSkiaEncoder
? typeof(SkiaEncoder)
: typeof(NullImageEncoder);
- ServiceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
+ serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType);
// Log a warning if the Skia encoder could not be used
if (!useSkiaEncoder)
{
- Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
+ Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
}
- ServiceCollection.AddDbContextPool<JellyfinDb>(
- options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
+ serviceCollection.AddDbContextPool<JellyfinDb>(
+ options => options
+ .UseLoggerFactory(LoggerFactory)
+ .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
- ServiceCollection.AddEventServices();
- ServiceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
- ServiceCollection.AddSingleton<IEventManager, EventManager>();
- ServiceCollection.AddSingleton<JellyfinDbProvider>();
+ serviceCollection.AddEventServices();
+ serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
+ serviceCollection.AddSingleton<IEventManager, EventManager>();
+ serviceCollection.AddSingleton<JellyfinDbProvider>();
- ServiceCollection.AddSingleton<IActivityManager, ActivityManager>();
- ServiceCollection.AddSingleton<IUserManager, UserManager>();
- ServiceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+ serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
+ serviceCollection.AddSingleton<IUserManager, UserManager>();
+ serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+ serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
// TODO search the assemblies instead of adding them manually?
- ServiceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
- ServiceCollection.AddSingleton<IWebSocketListener, ActivityLogWebSocketListener>();
- ServiceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
- ServiceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, ActivityLogWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
+ serviceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
- base.RegisterServices();
+ serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+
+ serviceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>();
+
+ base.RegisterServices(serviceCollection);
}
/// <inheritdoc />
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 924b250ce..fa98fda69 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -7,6 +7,7 @@ using System.Net.Sockets;
using System.Reflection;
using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
+using Jellyfin.Api.Auth.AnonymousLanAccessPolicy;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.DownloadPolicy;
using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
@@ -21,11 +22,11 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions.Json;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
@@ -61,6 +62,7 @@ namespace Jellyfin.Server.Extensions
serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeOrIgnoreParentalControlSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
@@ -157,6 +159,13 @@ namespace Jellyfin.Server.Extensions
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
});
+ options.AddPolicy(
+ Policies.AnonymousLanAccessPolicy,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new AnonymousLanAccessRequirement());
+ });
});
}
@@ -188,7 +197,8 @@ namespace Jellyfin.Server.Extensions
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
- options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
+
if (config.KnownProxies.Length == 0)
{
options.KnownNetworks.Clear();
@@ -278,7 +288,7 @@ namespace Jellyfin.Server.Extensions
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
- Name = "X-Emby-Authorization",
+ Name = "Authorization",
Description = "API key header parameter"
});
@@ -303,7 +313,7 @@ namespace Jellyfin.Server.Extensions
{
description.TryGetMethodInfo(out MethodInfo methodInfo);
// Attribute name, method name, none.
- return description?.ActionDescriptor?.AttributeRouteInfo?.Name
+ return description?.ActionDescriptor.AttributeRouteInfo?.Name
?? methodInfo?.Name
?? null;
});
@@ -341,7 +351,7 @@ namespace Jellyfin.Server.Extensions
{
foreach (var address in host.GetAddresses())
{
- AddIpAddress(config, options, addr.Address, addr.PrefixLength);
+ AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);
}
}
}
@@ -397,7 +407,7 @@ namespace Jellyfin.Server.Extensions
Type = "object",
Properties = typeof(ImageType).GetEnumNames().ToDictionary(
name => name,
- name => new OpenApiSchema
+ _ => new OpenApiSchema
{
Type = "object",
AdditionalProperties = new OpenApiSchema
@@ -406,6 +416,18 @@ namespace Jellyfin.Server.Extensions
}
})
});
+
+ // Support dictionary with nullable string value.
+ options.MapType<Dictionary<string, string?>>(() =>
+ new OpenApiSchema
+ {
+ Type = "object",
+ AdditionalProperties = new OpenApiSchema
+ {
+ Type = "string",
+ Nullable = true
+ }
+ });
}
}
}
diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
index c349e3dca..ea8c5ecdb 100644
--- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs
@@ -1,4 +1,4 @@
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
index 0480f5e0e..03ca7dda7 100644
--- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
+++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs
@@ -1,5 +1,5 @@
using System.Net.Mime;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
new file mode 100644
index 000000000..73a619b8d
--- /dev/null
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -0,0 +1,144 @@
+// The MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+//
+// All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Jellyfin.Server.Infrastructure
+{
+ /// <inheritdoc />
+ public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class.
+ /// </summary>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override FileMetadata GetFileInfo(string path)
+ {
+ var fileInfo = new FileInfo(path);
+ var length = fileInfo.Length;
+ // This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371
+ if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
+ {
+ using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ length = RandomAccess.GetLength(fileHandle);
+ }
+
+ return new FileMetadata
+ {
+ Exists = fileInfo.Exists,
+ Length = length,
+ LastModified = fileInfo.LastWriteTimeUtc
+ };
+ }
+
+ /// <inheritdoc />
+ protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (range != null && rangeLength == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
+ if (!IsSymLink(result.FileName))
+ {
+ return base.WriteFileAsync(context, result, range, rangeLength);
+ }
+
+ var response = context.HttpContext.Response;
+
+ if (range != null)
+ {
+ return SendFileAsync(
+ result.FileName,
+ response,
+ offset: range.From ?? 0L,
+ count: rangeLength);
+ }
+
+ return SendFileAsync(
+ result.FileName,
+ response,
+ offset: 0,
+ count: null);
+ }
+
+ private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
+ {
+ var fileInfo = GetFileInfo(filePath);
+ if (offset < 0 || offset > fileInfo.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
+ }
+
+ if (count.HasValue
+ && (count.Value < 0 || count.Value > fileInfo.Length - offset))
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
+ }
+
+ // Copied from SendFileFallback.SendFileAsync
+ const int BufferSize = 1024 * 16;
+
+ await using var fileStream = new FileStream(
+ filePath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.ReadWrite,
+ bufferSize: BufferSize,
+ options: FileOptions.Asynchronous | FileOptions.SequentialScan);
+
+ fileStream.Seek(offset, SeekOrigin.Begin);
+ await StreamCopyOperation
+ .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
+ .ConfigureAwait(true);
+ }
+
+ private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index ea782cb66..1638310fd 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -8,15 +8,10 @@
<PropertyGroup>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
- <!-- <DisableImplicitAspNetCoreAnalyzers>true</DisableImplicitAspNetCoreAnalyzers> -->
</PropertyGroup>
<ItemGroup>
@@ -36,20 +31,20 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.7" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.7" />
- <PackageReference Include="prometheus-net" Version="4.1.1" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.1" />
+ <PackageReference Include="prometheus-net" Version="5.0.2" />
+ <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
- <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
- <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
- <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
- <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
+ <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
+ <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
+ <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
+ <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" />
- <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.4" />
+ <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index 2eef223e5..3e5982eed 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -58,9 +58,12 @@ namespace Jellyfin.Server.Middleware
return;
}
- if (!startsWithBaseUrl)
+ if (!startsWithBaseUrl
+ || localPath.Length == baseUrlPrefix.Length
+ // Local path is /baseUrl/
+ || (localPath.Length == baseUrlPrefix.Length + 1 && localPath[^1] == '/'))
{
- // Always redirect back to the default path if the base prefix is invalid or missing
+ // Always redirect back to the default path if the base prefix is invalid, missing, or is the full path.
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]);
return;
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index f6c76e4d9..db7877c31 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
@@ -137,11 +137,6 @@ namespace Jellyfin.Server.Middleware
private string NormalizeExceptionMessage(string msg)
{
- if (msg == null)
- {
- return string.Empty;
- }
-
// Strip any information we don't want to reveal
return msg.Replace(
_configuration.ApplicationPaths.ProgramSystemPath,
diff --git a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
index fd0ebbf43..cdd86e28e 100644
--- a/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
+++ b/Jellyfin.Server/Middleware/QueryStringDecodingMiddleware.cs
@@ -27,7 +27,11 @@ namespace Jellyfin.Server.Middleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
- httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(httpContext.Features.Get<IQueryFeature>()));
+ var feature = httpContext.Features.Get<IQueryFeature>();
+ if (feature != null)
+ {
+ httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature));
+ }
await _next(httpContext).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
index 74874da1b..da9b69136 100644
--- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
+++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs
@@ -68,7 +68,7 @@ namespace Jellyfin.Server.Middleware
if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
{
_logger.LogWarning(
- "Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}",
+ "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
context.Request.GetDisplayUrl(),
context.GetNormalizedRemoteIp(),
watch.Elapsed,
diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
index 310a3d31a..2f1d79157 100644
--- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
+++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs
@@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Web;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
@@ -52,20 +51,14 @@ namespace Jellyfin.Server.Middleware
return;
}
- // Unencode and re-parse querystring.
- var unencodedKey = HttpUtility.UrlDecode(key);
-
- if (string.Equals(unencodedKey, key, StringComparison.Ordinal))
+ if (!key.Contains('=', StringComparison.Ordinal))
{
- // Don't do anything if it's not encoded.
_store = value;
return;
}
var pairs = new Dictionary<string, StringValues>();
- var queryString = unencodedKey.SpanSplit('&');
-
- foreach (var pair in queryString)
+ foreach (var pair in key.SpanSplit('&'))
{
var i = pair.IndexOf('=');
if (i == -1)
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index cf938ab8c..e9a45c140 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -1,6 +1,11 @@
using System;
+using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using Emby.Server.Implementations;
+using Emby.Server.Implementations.Serialization;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -12,6 +17,14 @@ namespace Jellyfin.Server.Migrations
public sealed class MigrationRunner
{
/// <summary>
+ /// The list of known pre-startup migrations, in order of applicability.
+ /// </summary>
+ private static readonly Type[] _preStartupMigrationTypes =
+ {
+ typeof(PreStartupRoutines.CreateNetworkConfiguration)
+ };
+
+ /// <summary>
/// The list of known migrations, in order of applicability.
/// </summary>
private static readonly Type[] _migrationTypes =
@@ -25,7 +38,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb),
typeof(Routines.RemoveDownloadImagesInAdvance),
- typeof(Routines.AddPeopleQueryIndex)
+ typeof(Routines.AddPeopleQueryIndex),
+ typeof(Routines.MigrateAuthenticationDb)
};
/// <summary>
@@ -40,17 +54,55 @@ namespace Jellyfin.Server.Migrations
.Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
.OfType<IMigrationRoutine>()
.ToArray();
- var migrationOptions = ((IConfigurationManager)host.ConfigurationManager).GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
- if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
+ var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
+ HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger);
+ PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger);
+ }
+
+ /// <summary>
+ /// Run all needed pre-startup migrations.
+ /// </summary>
+ /// <param name="appPaths">Application paths.</param>
+ /// <param name="loggerFactory">Factory for making the logger.</param>
+ public static void RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger<MigrationRunner>();
+ var migrations = _preStartupMigrationTypes
+ .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory))
+ .OfType<IMigrationRoutine>()
+ .ToArray();
+
+ var xmlSerializer = new MyXmlSerializer();
+ var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml");
+ var migrationOptions = File.Exists(migrationConfigPath)
+ ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
+ : new MigrationOptions();
+
+ // We have to deserialize it manually since the configuration manager may overwrite it
+ var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
+ ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
+ : new ServerConfiguration();
+
+ HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger);
+ PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger);
+ }
+
+ private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger)
+ {
+ if (isStartWizardCompleted || migrationOptions.Applied.Count != 0)
{
- // If startup wizard is not finished, this is a fresh install.
- // Don't run any migrations, just mark all of them as applied.
- logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
- migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
- host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+ return;
}
+ // If startup wizard is not finished, this is a fresh install.
+ var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray();
+ logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}", onlyOldInstalls.Select(m => m.Name));
+ migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name)));
+ }
+
+ private static void PerformMigrations(IMigrationRoutine[] migrations, MigrationOptions migrationOptions, Action<MigrationOptions> saveConfiguration, ILogger logger)
+ {
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
for (var i = 0; i < migrations.Length; i++)
@@ -77,7 +129,7 @@ namespace Jellyfin.Server.Migrations
// Mark the migration as completed
logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
- host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+ saveConfiguration(migrationOptions);
logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
}
}
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
new file mode 100644
index 000000000..a951f751e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
@@ -0,0 +1,138 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using Emby.Server.Implementations;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines;
+
+/// <inheritdoc />
+public class CreateNetworkConfiguration : IMigrationRoutine
+{
+ private readonly ServerApplicationPaths _applicationPaths;
+ private readonly ILogger<CreateNetworkConfiguration> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CreateNetworkConfiguration"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public CreateNetworkConfiguration(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger<CreateNetworkConfiguration>();
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84");
+
+ /// <inheritdoc />
+ public string Name => nameof(CreateNetworkConfiguration);
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "network.xml");
+ if (File.Exists(path))
+ {
+ _logger.LogDebug("Network configuration file already exists, skipping");
+ return;
+ }
+
+ var serverConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("ServerConfiguration"));
+ using var xmlReader = XmlReader.Create(_applicationPaths.SystemConfigurationFilePath);
+ var networkSettings = serverConfigSerializer.Deserialize(xmlReader);
+
+ var networkConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("NetworkConfiguration"));
+ var xmlWriterSettings = new XmlWriterSettings { Indent = true };
+ using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+ networkConfigSerializer.Serialize(xmlWriter, networkSettings);
+ }
+
+#pragma warning disable CS1591
+ public sealed class OldNetworkConfiguration
+ {
+ public const int DefaultHttpPort = 8096;
+
+ public const int DefaultHttpsPort = 8920;
+
+ private string _baseUrl = string.Empty;
+
+ public bool RequireHttps { get; set; }
+
+ public string CertificatePath { get; set; } = string.Empty;
+
+ public string CertificatePassword { get; set; } = string.Empty;
+
+ public string BaseUrl
+ {
+ get => _baseUrl;
+ set
+ {
+ // Normalize the start of the string
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ // If baseUrl is empty, set an empty prefix string
+ _baseUrl = string.Empty;
+ return;
+ }
+
+ if (value[0] != '/')
+ {
+ // If baseUrl was not configured with a leading slash, append one for consistency
+ value = "/" + value;
+ }
+
+ // Normalize the end of the string
+ if (value[^1] == '/')
+ {
+ // If baseUrl was configured with a trailing slash, remove it for consistency
+ value = value.Remove(value.Length - 1);
+ }
+
+ _baseUrl = value;
+ }
+ }
+
+ public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
+
+ public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
+
+ public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
+
+ public bool EnableHttps { get; set; }
+
+ public int PublicPort { get; set; } = DefaultHttpPort;
+
+ public bool EnableIPV6 { get; set; }
+
+ public bool EnableIPV4 { get; set; } = true;
+
+ public bool IgnoreVirtualInterfaces { get; set; } = true;
+
+ public string VirtualInterfaceNames { get; set; } = "vEthernet*";
+
+ public bool TrustAllIP6Interfaces { get; set; }
+
+ public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
+
+ public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
+
+ public bool IsRemoteIPFilterBlacklist { get; set; }
+
+ public bool EnableUPnP { get; set; }
+
+ public bool EnableRemoteAccess { get; set; } = true;
+
+ public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
+
+ public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
+
+ public string[] KnownProxies { get; set; } = Array.Empty<string>();
+ }
+#pragma warning restore CS1591
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
index 6048160c6..9e22978ae 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
@@ -92,7 +92,7 @@ namespace Jellyfin.Server.Migrations.Routines
if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid))
{
// This is not a valid Guid, see if it is an internal ID from an old Emby schema
- _logger.LogWarning("Invalid Guid in UserId column: ", entry[6].ToString());
+ _logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString());
using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
statement.TryBind("@Id", entry[6].ToString());
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
new file mode 100644
index 000000000..21f153623
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Server.Implementations;
+using MediaBrowser.Controller;
+using Microsoft.Extensions.Logging;
+using SQLitePCL.pretty;
+
+namespace Jellyfin.Server.Migrations.Routines
+{
+ /// <summary>
+ /// A migration that moves data from the authentication database into the new schema.
+ /// </summary>
+ public class MigrateAuthenticationDb : IMigrationRoutine
+ {
+ private const string DbFilename = "authentication.db";
+
+ private readonly ILogger<MigrateAuthenticationDb> _logger;
+ private readonly JellyfinDbProvider _dbProvider;
+ private readonly IServerApplicationPaths _appPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateAuthenticationDb"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="dbProvider">The database provider.</param>
+ /// <param name="appPaths">The server application paths.</param>
+ public MigrateAuthenticationDb(ILogger<MigrateAuthenticationDb> logger, JellyfinDbProvider dbProvider, IServerApplicationPaths appPaths)
+ {
+ _logger = logger;
+ _dbProvider = dbProvider;
+ _appPaths = appPaths;
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22");
+
+ /// <inheritdoc />
+ public string Name => "MigrateAuthenticationDatabase";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ var dataPath = _appPaths.DataPath;
+ using (var connection = SQLite3.Open(
+ Path.Combine(dataPath, DbFilename),
+ ConnectionFlags.ReadOnly,
+ null))
+ {
+ using var dbContext = _dbProvider.CreateContext();
+
+ var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
+
+ foreach (var row in authenticatedDevices)
+ {
+ if (row[6].IsDbNull())
+ {
+ dbContext.ApiKeys.Add(new ApiKey(row[3].ToString())
+ {
+ AccessToken = row[1].ToString(),
+ DateCreated = row[9].ToDateTime(),
+ DateLastActivity = row[10].ToDateTime()
+ });
+ }
+ else
+ {
+ dbContext.Devices.Add(new Device(
+ new Guid(row[6].ToString()),
+ row[3].ToString(),
+ row[4].ToString(),
+ row[5].ToString(),
+ row[2].ToString())
+ {
+ AccessToken = row[1].ToString(),
+ IsActive = row[8].ToBool(),
+ DateCreated = row[9].ToDateTime(),
+ DateLastActivity = row[10].ToDateTime()
+ });
+ }
+ }
+
+ var deviceOptions = connection.Query("SELECT * FROM Devices");
+ var deviceIds = new HashSet<string>();
+ foreach (var row in deviceOptions)
+ {
+ if (row[2].IsDbNull())
+ {
+ continue;
+ }
+
+ var deviceId = row[2].ToString();
+ if (deviceIds.Contains(deviceId))
+ {
+ continue;
+ }
+
+ deviceIds.Add(deviceId);
+
+ dbContext.DeviceOptions.Add(new DeviceOptions(deviceId)
+ {
+ CustomName = row[1].IsDbNull() ? null : row[1].ToString()
+ });
+ }
+
+ dbContext.SaveChanges();
+ }
+
+ try
+ {
+ File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
+
+ var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
+ if (File.Exists(journalPath))
+ {
+ File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
+ }
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error renaming legacy activity log database to 'authentication.db.old'");
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index e25d29122..74f2349f5 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -10,7 +9,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -115,13 +114,14 @@ namespace Jellyfin.Server.Migrations.Routines
}
var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
+ && !string.IsNullOrEmpty(version)
? chromecastDict[version]
: ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion");
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{
- IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
+ IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : null,
ShowBackdrop = dto.ShowBackdrop,
ShowSidebar = dto.ShowSidebar,
ScrollDirection = dto.ScrollDirection,
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index 96bd2ccc4..9b2d603c7 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -1,15 +1,15 @@
using System;
using System.IO;
using Emby.Server.Implementations.Data;
-using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions.Json;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Users;
-using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider;
- private readonly MyXmlSerializer _xmlSerializer;
+ private readonly IXmlSerializer _xmlSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Migrations.Routines
ILogger<MigrateUserDb> logger,
IServerApplicationPaths paths,
JellyfinDbProvider provider,
- MyXmlSerializer xmlSerializer)
+ IXmlSerializer xmlSerializer)
{
_logger = logger;
_paths = paths;
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3a3d7415b..f40526e22 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -5,17 +5,16 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.IO;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -121,11 +120,11 @@ namespace Jellyfin.Server
// Log uncaught exceptions to the logging instead of std error
AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
- AppDomain.CurrentDomain.UnhandledException += (sender, e)
+ AppDomain.CurrentDomain.UnhandledException += (_, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
// Intercept Ctrl+C and Ctrl+Break
- Console.CancelKeyPress += (sender, e) =>
+ Console.CancelKeyPress += (_, e) =>
{
if (_tokenSource.IsCancellationRequested)
{
@@ -139,7 +138,7 @@ namespace Jellyfin.Server
};
// Register a SIGTERM handler
- AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
+ AppDomain.CurrentDomain.ProcessExit += (_, _) =>
{
if (_tokenSource.IsCancellationRequested)
{
@@ -157,34 +156,37 @@ namespace Jellyfin.Server
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
+ // If hosting the web client, validate the client content path
+ if (startupConfig.HostWebClient())
+ {
+ string? webContentPath = appPaths.WebPath;
+ if (!Directory.Exists(webContentPath) || !Directory.EnumerateFiles(webContentPath).Any())
+ {
+ _logger.LogError(
+ "The server is expected to host the web client, but the provided content directory is either " +
+ "invalid or empty: {WebContentPath}. If you do not want to host the web client with the " +
+ "server, you may set the '--nowebclient' command line flag, or set" +
+ "'{ConfigKey}=false' in your config settings.",
+ webContentPath,
+ ConfigurationExtensions.HostWebClientKey);
+ Environment.ExitCode = 1;
+ return;
+ }
+ }
+
PerformStaticInitialization();
- var serviceCollection = new ServiceCollection();
+ Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory);
var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
- startupConfig,
- new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- serviceCollection);
+ startupConfig);
try
{
- // If hosting the web client, validate the client content path
- if (startupConfig.HostWebClient())
- {
- string? webContentPath = appHost.ConfigurationManager.ApplicationPaths.WebPath;
- if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0)
- {
- throw new InvalidOperationException(
- "The server is expected to host the web client, but the provided content directory is either " +
- $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " +
- "server, you may set the '--nowebclient' command line flag, or set" +
- $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings.");
- }
- }
-
- appHost.Init();
+ var serviceCollection = new ServiceCollection();
+ appHost.Init(serviceCollection);
var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build();
@@ -195,9 +197,9 @@ namespace Jellyfin.Server
try
{
- await webHost.StartAsync().ConfigureAwait(false);
+ await webHost.StartAsync(_tokenSource.Token).ConfigureAwait(false);
}
- catch
+ catch (Exception ex) when (ex is not TaskCanceledException)
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
throw;
@@ -224,7 +226,7 @@ namespace Jellyfin.Server
{
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
- using var context = new JellyfinDbProvider(appHost.ServiceProvider, appPaths).CreateContext();
+ using var context = appHost.Resolve<JellyfinDbProvider>().CreateContext();
if (context.Database.IsSqlite())
{
context.Database.ExecuteSqlRaw("PRAGMA optimize");
@@ -318,8 +320,8 @@ namespace Jellyfin.Server
}
}
- // Bind to unix socket (only on macOS and Linux)
- if (startupConfig.UseUnixSocket() && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ // Bind to unix socket (only on unix systems)
+ if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix)
{
var socketPath = startupConfig.GetUnixSocketPath();
if (string.IsNullOrEmpty(socketPath))
@@ -404,7 +406,7 @@ namespace Jellyfin.Server
{
if (options.DataDir != null
|| Directory.Exists(Path.Combine(dataDir, "config"))
- || RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ || OperatingSystem.IsWindows())
{
// Hang config folder off already set dataDir
configDir = Path.Combine(dataDir, "config");
@@ -442,7 +444,7 @@ namespace Jellyfin.Server
if (string.IsNullOrEmpty(cacheDir))
{
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ if (OperatingSystem.IsWindows())
{
// Hang cache folder off already set dataDir
cacheDir = Path.Combine(dataDir, "cache");
@@ -543,11 +545,11 @@ namespace Jellyfin.Server
// Get a stream of the resource contents
// NOTE: The .csproj name is used instead of the assembly name in the resource path
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
- await using Stream? resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
+ await using Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
// Copy the resource contents to the expected file path for the config file
- await using Stream dst = File.Open(configPath, FileMode.CreateNew);
+ await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await resource.CopyToAsync(dst).ConfigureAwait(false);
}
@@ -594,7 +596,7 @@ namespace Jellyfin.Server
try
{
// Serilog.Log is used by SerilogLoggerFactory when no logger is specified
- Serilog.Log.Logger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
@@ -602,7 +604,7 @@ namespace Jellyfin.Server
}
catch (Exception ex)
{
- Serilog.Log.Logger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
.WriteTo.Async(x => x.File(
Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
@@ -613,7 +615,7 @@ namespace Jellyfin.Server
.Enrich.WithThreadId()
.CreateLogger();
- Serilog.Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
+ Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
}
}
@@ -648,7 +650,7 @@ namespace Jellyfin.Server
private static string NormalizeCommandLineArgument(string arg)
{
- if (!arg.Contains(" ", StringComparison.OrdinalIgnoreCase))
+ if (!arg.Contains(' ', StringComparison.Ordinal))
{
return arg;
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 60cdc2f6f..8085c2630 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -7,6 +7,7 @@ using System.Text;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Infrastructure;
using Jellyfin.Server.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -14,6 +15,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -56,6 +59,9 @@ namespace Jellyfin.Server
{
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
+
+ // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
+ services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinApiSwagger();
diff --git a/Jellyfin.sln b/Jellyfin.sln
index 9fbd9d266..4626601c3 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -83,6 +83,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Integration
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Providers.Tests", "tests\Jellyfin.Providers.Tests\Jellyfin.Providers.Tests.csproj", "{A964008C-2136-4716-B6CB-B3426C22320A}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Extensions", "src\Jellyfin.Extensions\Jellyfin.Extensions.csproj", "{750B8757-BE3D-4F8C-941A-FBAD94904ADA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Extensions.Tests", "tests\Jellyfin.Extensions.Tests\Jellyfin.Extensions.Tests.csproj", "{332A5C7A-F907-47CA-910E-BE6F7371B9E0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -229,6 +235,14 @@ Global
{A964008C-2136-4716-B6CB-B3426C22320A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A964008C-2136-4716-B6CB-B3426C22320A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {750B8757-BE3D-4F8C-941A-FBAD94904ADA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -247,6 +261,8 @@ Global
{3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{68B0B823-A5AC-4E8B-82EA-965AAC7BF76E} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{A964008C-2136-4716-B6CB-B3426C22320A} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {750B8757-BE3D-4F8C-941A-FBAD94904ADA} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
+ {332A5C7A-F907-47CA-910E-BE6F7371B9E0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/MediaBrowser.Common/Cryptography/CryptoExtensions.cs b/MediaBrowser.Common/Cryptography/CryptoExtensions.cs
deleted file mode 100644
index 157b0ed10..000000000
--- a/MediaBrowser.Common/Cryptography/CryptoExtensions.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Collections.Generic;
-using System.Globalization;
-using System.Text;
-using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.Cryptography.Constants;
-
-namespace MediaBrowser.Common.Cryptography
-{
- /// <summary>
- /// Class containing extension methods for working with Jellyfin cryptography objects.
- /// </summary>
- public static class CryptoExtensions
- {
- /// <summary>
- /// Creates a new <see cref="PasswordHash" /> instance.
- /// </summary>
- /// <param name="cryptoProvider">The <see cref="ICryptoProvider" /> instance used.</param>
- /// <param name="password">The password that will be hashed.</param>
- /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns>
- public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password)
- {
- byte[] salt = cryptoProvider.GenerateSalt();
- return new PasswordHash(
- cryptoProvider.DefaultHashMethod,
- cryptoProvider.ComputeHashWithDefaultMethod(
- Encoding.UTF8.GetBytes(password),
- salt),
- salt,
- new Dictionary<string, string>
- {
- { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
- });
- }
- }
-}
diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
index c74787122..08e01bfd6 100644
--- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
@@ -40,7 +40,7 @@ namespace MediaBrowser.Common.Extensions
// Add an event handler for the process exit event
var tcs = new TaskCompletionSource<bool>();
- process.Exited += (sender, args) => tcs.TrySetResult(true);
+ process.Exited += (_, _) => tcs.TrySetResult(true);
// Return immediately if the process has already exited
if (process.HasExitedSafe())
diff --git a/MediaBrowser.MediaEncoding/FfmpegException.cs b/MediaBrowser.Common/FfmpegException.cs
index 1697fd33a..be420196d 100644
--- a/MediaBrowser.MediaEncoding/FfmpegException.cs
+++ b/MediaBrowser.Common/FfmpegException.cs
@@ -1,6 +1,6 @@
using System;
-namespace MediaBrowser.MediaEncoding
+namespace MediaBrowser.Common
{
/// <summary>
/// Represents errors that occur during interaction with FFmpeg.
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 192a77611..53683cdbd 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
@@ -137,13 +138,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Initializes this instance.
/// </summary>
- void Init();
-
- /// <summary>
- /// Creates the instance.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- object CreateInstance(Type type);
+ /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
+ void Init(IServiceCollection serviceCollection);
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs b/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs
deleted file mode 100644
index bd9600110..000000000
--- a/MediaBrowser.Common/Json/Converters/JsonGuidConverter.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Globalization;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
-{
- /// <summary>
- /// Converts a GUID object or value to/from JSON.
- /// </summary>
- public class JsonGuidConverter : JsonConverter<Guid>
- {
- /// <inheritdoc />
- public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var guidStr = reader.GetString();
- return guidStr == null ? Guid.Empty : new Guid(guidStr);
- }
-
- /// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
- {
- writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
- }
- }
-}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 0299a8456..4ed44baef 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -19,9 +19,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@@ -29,13 +29,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs
index 5db8817ee..1f125f2b1 100644
--- a/MediaBrowser.Common/Net/IPHost.cs
+++ b/MediaBrowser.Common/Net/IPHost.cs
@@ -4,7 +4,6 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
-using System.Threading.Tasks;
namespace MediaBrowser.Common.Net
{
@@ -79,16 +78,11 @@ namespace MediaBrowser.Common.Net
/// </summary>
public override byte PrefixLength
{
- get
- {
- return (byte)(ResolveHost() ? 128 : 32);
- }
+ get => (byte)(ResolveHost() ? 128 : 32);
- set
- {
- // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length,
- // which is automatically determined by it's IP type. Anything else is meaningless.
- }
+ // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length,
+ // which is automatically determined by it's IP type. Anything else is meaningless.
+ set => throw new NotImplementedException();
}
/// <summary>
@@ -201,7 +195,7 @@ namespace MediaBrowser.Common.Net
return res;
}
- throw new InvalidCastException("Host does not contain a valid value. {host}");
+ throw new InvalidCastException($"Host does not contain a valid value. {host}");
}
/// <summary>
@@ -226,7 +220,7 @@ namespace MediaBrowser.Common.Net
return res;
}
- throw new InvalidCastException("Host does not contain a valid value. {host}");
+ throw new InvalidCastException($"Host does not contain a valid value. {host}");
}
/// <summary>
@@ -354,7 +348,7 @@ namespace MediaBrowser.Common.Net
}
}
- output = output[0..^1];
+ output = output[..^1];
if (moreThanOne)
{
@@ -405,7 +399,7 @@ namespace MediaBrowser.Common.Net
if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout)))
{
_lastResolved = DateTime.UtcNow;
- ResolveHostInternal().GetAwaiter().GetResult();
+ ResolveHostInternal();
Resolved = true;
}
@@ -415,30 +409,31 @@ namespace MediaBrowser.Common.Net
/// <summary>
/// Task that looks up a Host name and returns its IP addresses.
/// </summary>
- /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
- private async Task ResolveHostInternal()
+ private void ResolveHostInternal()
{
- if (!string.IsNullOrEmpty(HostName))
+ var hostName = HostName;
+ if (string.IsNullOrEmpty(hostName))
+ {
+ return;
+ }
+
+ // Resolves the host name - so save a DNS lookup.
+ if (string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase))
{
- // Resolves the host name - so save a DNS lookup.
- if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase))
+ _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback };
+ return;
+ }
+
+ if (Uri.CheckHostName(hostName) == UriHostNameType.Dns)
+ {
+ try
{
- _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback };
- return;
+ _addresses = Dns.GetHostEntry(hostName).AddressList;
}
-
- if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns))
+ catch (SocketException ex)
{
- try
- {
- IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false);
- _addresses = ip.AddressList;
- }
- catch (SocketException ex)
- {
- // Log and then ignore socket errors, as the result value will just be an empty array.
- Debug.WriteLine("GetHostEntryAsync failed with {Message}.", ex.Message);
- }
+ // Log and then ignore socket errors, as the result value will just be an empty array.
+ Debug.WriteLine("GetHostAddresses failed with {Message}.", ex.Message);
}
}
}
diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
index 8a6d28e0f..afda83a7c 100644
--- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs
+++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
@@ -47,10 +47,10 @@ namespace MediaBrowser.Common.Plugins
var assemblyFilePath = assembly.Location;
var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
- if (!Directory.Exists(dataFolderPath) && Version != null)
+ if (Version != null && !Directory.Exists(dataFolderPath))
{
// Try again with the version number appended to the folder name.
- dataFolderPath = dataFolderPath + "_" + Version.ToString();
+ dataFolderPath += "_" + Version.ToString();
}
SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs
index 33d09ed38..487b5a6d2 100644
--- a/MediaBrowser.Common/Providers/ProviderIdParsers.cs
+++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs
@@ -18,7 +18,7 @@ namespace MediaBrowser.Common.Providers
/// <param name="text">The text to parse.</param>
/// <param name="imdbId">The parsed IMDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
- public static bool TryFindImdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> imdbId)
+ public static bool TryFindImdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> imdbId)
{
// imdb id is at least 9 chars (tt + 7 numbers)
while (text.Length >= 2 + ImdbMinNumbers)
@@ -62,7 +62,7 @@ namespace MediaBrowser.Common.Providers
/// <param name="text">The text with the url to parse.</param>
/// <param name="tmdbId">The parsed TMDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
- public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId)
+ public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId)
=> TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId);
/// <summary>
@@ -71,7 +71,7 @@ namespace MediaBrowser.Common.Providers
/// <param name="text">The text with the url to parse.</param>
/// <param name="tmdbId">The parsed TMDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
- public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tmdbId)
+ public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId)
=> TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId);
/// <summary>
@@ -80,7 +80,7 @@ namespace MediaBrowser.Common.Providers
/// <param name="text">The text with the url to parse.</param>
/// <param name="tvdbId">The parsed TVDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
- public static bool TryFindTvdbId(ReadOnlySpan<char> text, [NotNullWhen(true)] out ReadOnlySpan<char> tvdbId)
+ public static bool TryFindTvdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tvdbId)
=> TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId);
private static bool TryFindProviderId(ReadOnlySpan<char> text, ReadOnlySpan<char> searchString, [NotNullWhen(true)] out ReadOnlySpan<char> providerId)
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index ffc274c5d..d273b54fc 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -1,9 +1,8 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -16,7 +15,7 @@ namespace MediaBrowser.Controller.BaseItemManager
{
private readonly IServerConfigurationManager _serverConfigurationManager;
- private int _metadataRefreshConcurrency = 0;
+ private int _metadataRefreshConcurrency;
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemManager"/> class.
@@ -56,12 +55,7 @@ namespace MediaBrowser.Controller.BaseItemManager
return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
- if (!libraryOptions.EnableInternetProviders)
- {
- return false;
- }
-
- var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+ var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
@@ -87,12 +81,7 @@ namespace MediaBrowser.Controller.BaseItemManager
return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
- if (!libraryOptions.EnableInternetProviders)
- {
- return false;
- }
-
- var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+ var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
}
@@ -101,7 +90,7 @@ namespace MediaBrowser.Controller.BaseItemManager
/// Called when the configuration is updated.
/// It will refresh the metadata throttler if the relevant config changed.
/// </summary>
- private void OnConfigurationUpdated(object sender, EventArgs e)
+ private void OnConfigurationUpdated(object? sender, EventArgs e)
{
int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency();
if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency)
@@ -114,6 +103,7 @@ namespace MediaBrowser.Controller.BaseItemManager
/// <summary>
/// Creates the metadata refresh throttler.
/// </summary>
+ [MemberNotNull(nameof(MetadataRefreshThrottler))]
private void SetupMetadataThrottler()
{
MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency);
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
index b2b36c040..e18994214 100644
--- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System.Threading;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
@@ -34,4 +32,4 @@ namespace MediaBrowser.Controller.BaseItemManager
/// <returns><c>true</c> if image fetcher is enabled, else false.</returns>
bool IsImageFetcherEnabled(BaseItem baseItem, LibraryOptions libraryOptions, string name);
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index 26a936be0..e6923b55c 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -17,6 +17,12 @@ namespace MediaBrowser.Controller.Channels
{
public class Channel : Folder
{
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ [JsonIgnore]
+ public override SourceType SourceType => SourceType.Channel;
+
public override bool IsVisible(User user)
{
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
@@ -39,12 +45,6 @@ namespace MediaBrowser.Controller.Channels
return base.IsVisible(user);
}
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override SourceType SourceType => SourceType.Channel;
-
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
try
diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
index 4d1e35f9e..55f80b240 100644
--- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
+++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
index 6b2077662..ca7721991 100644
--- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs
+++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
@@ -1,7 +1,6 @@
-#nullable disable
-
#pragma warning disable CS1591
+using System;
using System.Collections.Generic;
namespace MediaBrowser.Controller.Channels
@@ -10,10 +9,10 @@ namespace MediaBrowser.Controller.Channels
{
public ChannelItemResult()
{
- Items = new List<ChannelItemInfo>();
+ Items = Array.Empty<ChannelItemInfo>();
}
- public List<ChannelItemInfo> Items { get; set; }
+ public IReadOnlyList<ChannelItemInfo> Items { get; set; }
public int? TotalRecordCount { get; set; }
}
diff --git a/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
index 47277a8cc..64af8496c 100644
--- a/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
+++ b/MediaBrowser.Controller/Channels/IHasFolderAttributes.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
namespace MediaBrowser.Controller.Channels
{
diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
index 45cd08173..394996868 100644
--- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
+++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System.Collections.Generic;
using MediaBrowser.Model.Channels;
diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs
index f82e5b41a..c049bb97e 100644
--- a/MediaBrowser.Controller/Chapters/IChapterManager.cs
+++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs
@@ -12,6 +12,8 @@ namespace MediaBrowser.Controller.Chapters
/// <summary>
/// Saves the chapters.
/// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <param name="chapters">The set of chapters.</param>
void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
}
}
diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
new file mode 100644
index 000000000..dea1c2f32
--- /dev/null
+++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ClientEvent
+{
+ /// <inheritdoc />
+ public class ClientEventLogger : IClientEventLogger
+ {
+ private readonly IServerApplicationPaths _applicationPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClientEventLogger"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ public ClientEventLogger(IServerApplicationPaths applicationPaths)
+ {
+ _applicationPaths = applicationPaths;
+ }
+
+ /// <inheritdoc />
+ public async Task<string> WriteDocumentAsync(string clientName, string clientVersion, Stream fileContents)
+ {
+ var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
+ var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
+ await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
+ return fileName;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs
new file mode 100644
index 000000000..ad8a1bd24
--- /dev/null
+++ b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs
@@ -0,0 +1,23 @@
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.ClientEvent
+{
+ /// <summary>
+ /// The client event logger.
+ /// </summary>
+ public interface IClientEventLogger
+ {
+ /// <summary>
+ /// Writes a file to the log directory.
+ /// </summary>
+ /// <param name="clientName">The client name writing the document.</param>
+ /// <param name="clientVersion">The client version writing the document.</param>
+ /// <param name="fileContents">The file contents to write.</param>
+ /// <returns>The created file name.</returns>
+ Task<string> WriteDocumentAsync(
+ string clientName,
+ string clientVersion,
+ Stream fileContents);
+ }
+}
diff --git a/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
index 8155cf3db..e538fa4b3 100644
--- a/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
+++ b/MediaBrowser.Controller/Collections/CollectionModifiedEventArgs.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index 46bc37e7f..b8c33ee5a 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -16,22 +14,23 @@ namespace MediaBrowser.Controller.Collections
/// <summary>
/// Occurs when [collection created].
/// </summary>
- event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+ event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
/// <summary>
/// Occurs when [items added to collection].
/// </summary>
- event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+ event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
/// <summary>
/// Occurs when [items removed from collection].
/// </summary>
- event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+ event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
/// <summary>
/// Creates the collection.
/// </summary>
/// <param name="options">The options.</param>
+ /// <returns>BoxSet wrapped in an awaitable task.</returns>
Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options);
/// <summary>
diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
index 44e2c45dd..43ad04dba 100644
--- a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
+++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index 8096be1bd..8362db1a7 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -3,8 +3,11 @@
#pragma warning disable CS1591
using System;
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
@@ -16,32 +19,51 @@ namespace MediaBrowser.Controller.Devices
event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
/// <summary>
+ /// Creates a new device.
+ /// </summary>
+ /// <param name="device">The device to create.</param>
+ /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
+ Task<Device> CreateDevice(Device device);
+
+ /// <summary>
/// Saves the capabilities.
/// </summary>
- /// <param name="reportedId">The reported identifier.</param>
+ /// <param name="deviceId">The device id.</param>
/// <param name="capabilities">The capabilities.</param>
- void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
+ void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
/// <summary>
/// Gets the capabilities.
/// </summary>
- /// <param name="reportedId">The reported identifier.</param>
+ /// <param name="deviceId">The device id.</param>
/// <returns>ClientCapabilities.</returns>
- ClientCapabilities GetCapabilities(string reportedId);
+ ClientCapabilities GetCapabilities(string deviceId);
/// <summary>
/// Gets the device information.
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>DeviceInfo.</returns>
- DeviceInfo GetDevice(string id);
+ Task<DeviceInfo> GetDevice(string id);
+
+ /// <summary>
+ /// Gets devices based on the provided query.
+ /// </summary>
+ /// <param name="query">The device query.</param>
+ /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
+ Task<QueryResult<Device>> GetDevices(DeviceQuery query);
+
+ Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
/// <summary>
/// Gets the devices.
/// </summary>
- /// <param name="query">The query.</param>
+ /// <param name="userId">The user's id, or <c>null</c>.</param>
+ /// <param name="supportsSync">A value indicating whether the device supports sync, or <c>null</c>.</param>
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
- QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
+ Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync);
+
+ Task DeleteDevice(Device device);
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
@@ -51,8 +73,8 @@ namespace MediaBrowser.Controller.Devices
/// <returns>Whether the user can access the device.</returns>
bool CanAccessDevice(User user, string deviceId);
- void UpdateDeviceOptions(string deviceId, DeviceOptions options);
+ Task UpdateDeviceOptions(string deviceId, string deviceName);
- DeviceOptions GetDeviceOptions(string deviceId);
+ Task<DeviceOptions> GetDeviceOptions(string deviceId);
}
}
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
index b51dc255c..06da5ea09 100644
--- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs
+++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -22,7 +20,7 @@ namespace MediaBrowser.Controller.Dlna
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>DeviceProfile.</returns>
- DeviceProfile GetProfile(IHeaderDictionary headers);
+ DeviceProfile? GetProfile(IHeaderDictionary headers);
/// <summary>
/// Gets the default profile.
@@ -39,8 +37,9 @@ namespace MediaBrowser.Controller.Dlna
/// <summary>
/// Updates the profile.
/// </summary>
+ /// <param name="profileId">The profile id.</param>
/// <param name="profile">The profile.</param>
- void UpdateProfile(DeviceProfile profile);
+ void UpdateProfile(string profileId, DeviceProfile profile);
/// <summary>
/// Deletes the profile.
@@ -53,14 +52,14 @@ namespace MediaBrowser.Controller.Dlna
/// </summary>
/// <param name="id">The identifier.</param>
/// <returns>DeviceProfile.</returns>
- DeviceProfile GetProfile(string id);
+ DeviceProfile? GetProfile(string id);
/// <summary>
/// Gets the profile.
/// </summary>
/// <param name="deviceInfo">The device information.</param>
/// <returns>DeviceProfile.</returns>
- DeviceProfile GetProfile(DeviceIdentification deviceInfo);
+ DeviceProfile? GetProfile(DeviceIdentification deviceInfo);
/// <summary>
/// Gets the server description XML.
@@ -76,6 +75,6 @@ namespace MediaBrowser.Controller.Dlna
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>DlnaIconResponse.</returns>
- ImageStream GetIcon(string filename);
+ ImageStream? GetIcon(string filename);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 4e640d421..4e67cfee4 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -57,6 +57,15 @@ namespace MediaBrowser.Controller.Drawing
/// <summary>
/// Encode an image.
/// </summary>
+ /// <param name="inputPath">Input path of image.</param>
+ /// <param name="dateModified">Date modified.</param>
+ /// <param name="outputPath">Output path of image.</param>
+ /// <param name="autoOrient">Auto-orient image.</param>
+ /// <param name="orientation">Desired orientation of image.</param>
+ /// <param name="quality">Quality of encoded image.</param>
+ /// <param name="options">Image processing options.</param>
+ /// <param name="outputFormat">Image format of output.</param>
+ /// <returns>Path of encoded image.</returns>
string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
/// <summary>
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index c7f61a90b..7ca0e851b 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
- string GetImageCacheTag(BaseItem item, ChapterInfo info);
+ string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
index 5ee781ffa..f4c305799 100644
--- a/MediaBrowser.Controller/Drawing/ImageStream.cs
+++ b/MediaBrowser.Controller/Drawing/ImageStream.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1711, CS1591
using System;
using System.IO;
@@ -8,11 +8,16 @@ namespace MediaBrowser.Controller.Drawing
{
public class ImageStream : IDisposable
{
+ public ImageStream(Stream stream)
+ {
+ Stream = stream;
+ }
+
/// <summary>
- /// Gets or sets the stream.
+ /// Gets the stream.
/// </summary>
/// <value>The stream.</value>
- public Stream? Stream { get; set; }
+ public Stream Stream { get; }
/// <summary>
/// Gets or sets the format.
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index 61d796235..89aafc84f 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -1,4 +1,5 @@
#nullable disable
+#pragma warning disable CA1002
using System.Collections.Generic;
using Jellyfin.Data.Entities;
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index fe1bc62ab..9589f5245 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
using System;
using System.Collections.Concurrent;
@@ -18,33 +18,24 @@ namespace MediaBrowser.Controller.Entities
{
/// <summary>
/// Specialized folder that can have items added to it's children by external entities.
- /// Used for our RootFolder so plug-ins can add items.
+ /// Used for our RootFolder so plugins can add items.
/// </summary>
public class AggregateFolder : Folder
{
+ private readonly object _childIdsLock = new object();
+
+ /// <summary>
+ /// The _virtual children.
+ /// </summary>
+ private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
private bool _requiresRefresh;
+ private Guid[] _childrenIds = null;
public AggregateFolder()
{
PhysicalLocationsList = Array.Empty<string>();
}
- [JsonIgnore]
- public override bool IsPhysicalRoot => true;
-
- public override bool CanDelete()
- {
- return false;
- }
-
- [JsonIgnore]
- public override bool SupportsPlayedStatus => false;
-
- /// <summary>
- /// The _virtual children.
- /// </summary>
- private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
-
/// <summary>
/// Gets the virtual children.
/// </summary>
@@ -52,18 +43,26 @@ namespace MediaBrowser.Controller.Entities
public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren;
[JsonIgnore]
+ public override bool IsPhysicalRoot => true;
+
+ [JsonIgnore]
+ public override bool SupportsPlayedStatus => false;
+
+ [JsonIgnore]
public override string[] PhysicalLocations => PhysicalLocationsList;
public string[] PhysicalLocationsList { get; set; }
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
{
return CreateResolveArgs(directoryService, true).FileSystemChildren;
}
- private Guid[] _childrenIds = null;
- private readonly object _childIdsLock = new object();
-
protected override List<BaseItem> LoadChildren()
{
lock (_childIdsLock)
@@ -169,7 +168,7 @@ namespace MediaBrowser.Controller.Entities
/// Adds the virtual child.
/// </summary>
/// <param name="child">The child.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if child is null.</exception>
public void AddVirtualChild(BaseItem child)
{
if (child == null)
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index 576ab67a2..9d0187c8c 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1724, CA1826, CS1591
using System;
using System.Collections.Generic;
@@ -8,10 +8,8 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -25,6 +23,12 @@ namespace MediaBrowser.Controller.Entities.Audio
IHasLookupInfo<SongInfo>,
IHasMediaSources
{
+ public Audio()
+ {
+ Artists = Array.Empty<string>();
+ AlbumArtists = Array.Empty<string>();
+ }
+
/// <inheritdoc />
[JsonIgnore]
public IReadOnlyList<string> Artists { get; set; }
@@ -33,22 +37,11 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public IReadOnlyList<string> AlbumArtists { get; set; }
- public Audio()
- {
- Artists = Array.Empty<string>();
- AlbumArtists = Array.Empty<string>();
- }
-
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- return 1;
- }
-
[JsonIgnore]
public override bool SupportsPlayedStatus => true;
[JsonIgnore]
- public override bool SupportsPeople => false;
+ public override bool SupportsPeople => true;
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -62,11 +55,6 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override Folder LatestItemsIndexContainer => AlbumEntity;
- public override bool CanDownload()
- {
- return IsFileProtocol;
- }
-
[JsonIgnore]
public MusicAlbum AlbumEntity => FindParent<MusicAlbum>();
@@ -77,6 +65,16 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override string MediaType => Model.Entities.MediaType.Audio;
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ public override bool CanDownload()
+ {
+ return IsFileProtocol;
+ }
+
/// <summary>
/// Creates the name of the sort.
/// </summary>
@@ -126,15 +124,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.GetBlockUnratedType();
}
- public List<MediaStream> GetMediaStreams(MediaStreamType type)
- {
- return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
- {
- ItemId = Id,
- Type = type
- });
- }
-
public SongInfo GetLookupInfo()
{
var info = GetItemLookupInfo<SongInfo>();
@@ -146,11 +135,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return info;
}
- protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
- {
- var list = new List<Tuple<BaseItem, MediaSourceType>>();
- list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default));
- return list;
- }
+ protected override IEnumerable<(BaseItem, MediaSourceType)> GetAllItemsForMediaSources()
+ => new[] { ((BaseItem)this, MediaSourceType.Default) };
}
}
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
index db60c3071..c2dae5a2d 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
namespace MediaBrowser.Controller.Entities.Audio
{
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 610bce4f5..03d1f3304 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1721, CA1826, CS1591
using System;
using System.Collections.Generic;
@@ -23,18 +23,18 @@ namespace MediaBrowser.Controller.Entities.Audio
/// </summary>
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
{
- /// <inheritdoc />
- public IReadOnlyList<string> AlbumArtists { get; set; }
-
- /// <inheritdoc />
- public IReadOnlyList<string> Artists { get; set; }
-
public MusicAlbum()
{
Artists = Array.Empty<string>();
AlbumArtists = Array.Empty<string>();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> AlbumArtists { get; set; }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> Artists { get; set; }
+
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -44,6 +44,25 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true));
+ [JsonIgnore]
+ public override bool SupportsPlayedStatus => false;
+
+ [JsonIgnore]
+ public override bool SupportsCumulativeRunTimeTicks => true;
+
+ [JsonIgnore]
+ public string AlbumArtist => AlbumArtists.FirstOrDefault();
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ /// <summary>
+ /// Gets the tracks.
+ /// </summary>
+ /// <value>The tracks.</value>
+ [JsonIgnore]
+ public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
+
public MusicArtist GetMusicArtist(DtoOptions options)
{
var parents = GetParents();
@@ -64,25 +83,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return null;
}
- [JsonIgnore]
- public override bool SupportsPlayedStatus => false;
-
- [JsonIgnore]
- public override bool SupportsCumulativeRunTimeTicks => true;
-
- [JsonIgnore]
- public string AlbumArtist => AlbumArtists.FirstOrDefault();
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- /// <summary>
- /// Gets the tracks.
- /// </summary>
- /// <value>The tracks.</value>
- [JsonIgnore]
- public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
-
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return Tracks;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index c0cd81110..11b95b94b 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -8,9 +8,9 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@@ -44,6 +44,36 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
+ /// <summary>
+ /// Gets the folder containing the item.
+ /// If the item is a folder, it returns the folder itself.
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [JsonIgnore]
+ public override string ContainingFolderPath => Path;
+
+ [JsonIgnore]
+ public override IEnumerable<BaseItem> Children
+ {
+ get
+ {
+ if (IsAccessedByName)
+ {
+ return new List<BaseItem>();
+ }
+
+ return base.Children;
+ }
+ }
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
return 1;
@@ -58,27 +88,13 @@ namespace MediaBrowser.Controller.Entities.Audio
{
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Audio, BaseItemKind.MusicVideo, BaseItemKind.MusicAlbum };
query.ArtistIds = new[] { Id };
}
return LibraryManager.GetItemList(query);
}
- [JsonIgnore]
- public override IEnumerable<BaseItem> Children
- {
- get
- {
- if (IsAccessedByName)
- {
- return new List<BaseItem>();
- }
-
- return base.Children;
- }
- }
-
public override int GetChildCount(User user)
{
return IsAccessedByName ? 0 : base.GetChildCount(user);
@@ -114,14 +130,6 @@ namespace MediaBrowser.Controller.Entities.Audio
}
/// <summary>
- /// Gets the folder containing the item.
- /// If the item is a folder, it returns the folder itself.
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
- /// <summary>
/// Gets the user data key.
/// </summary>
/// <param name="item">The item.</param>
@@ -167,14 +175,6 @@ namespace MediaBrowser.Controller.Entities.Audio
return info;
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- public static string GetPath(string name)
- {
- return GetPath(name, true);
- }
-
public static string GetPath(string name, bool normalizeName)
{
// Trim the period at the end because windows will have a hard time with that
@@ -208,6 +208,8 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
+ /// <param name="replaceAllMetadata">Option to replace metadata.</param>
+ /// <returns>True if metadata changed.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index a682a2e58..73a25232e 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -5,7 +5,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
+using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities.Audio
@@ -15,19 +16,6 @@ namespace MediaBrowser.Controller.Entities.Audio
/// </summary>
public class MusicGenre : BaseItem, IItemByName
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
- return list;
- }
-
- public override string CreatePresentationUniqueKey()
- {
- return GetUserDataKeys()[0];
- }
-
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -45,6 +33,22 @@ namespace MediaBrowser.Controller.Entities.Audio
[JsonIgnore]
public override string ContainingFolderPath => Path;
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
return 1;
@@ -60,13 +64,10 @@ namespace MediaBrowser.Controller.Entities.Audio
return true;
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
- query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) };
+ query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist };
return LibraryManager.GetItemList(query);
}
@@ -106,6 +107,8 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
+ /// <param name="replaceAllMetadata">Option to replace metadata.</param>
+ /// <returns>True if metadata changed.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs
index 405284622..782481fbc 100644
--- a/MediaBrowser.Controller/Entities/AudioBook.cs
+++ b/MediaBrowser.Controller/Entities/AudioBook.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1724, CS1591
using System;
using System.Text.Json.Serialization;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 6137ddbf7..3ef2e5192 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1,27 +1,26 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, SA1401
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -39,6 +38,10 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
{
+ private BaseItemKind? _baseItemKind;
+
+ public const string ThemeSongFileName = "theme";
+
/// <summary>
/// The supported image extensions.
/// </summary>
@@ -60,6 +63,28 @@ namespace MediaBrowser.Controller.Entities
".ttml"
};
+ /// <summary>
+ /// Extra types that should be counted and displayed as "Special Features" in the UI.
+ /// </summary>
+ public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
+ {
+ Model.Entities.ExtraType.Unknown,
+ Model.Entities.ExtraType.BehindTheScenes,
+ Model.Entities.ExtraType.Clip,
+ Model.Entities.ExtraType.DeletedScene,
+ Model.Entities.ExtraType.Interview,
+ Model.Entities.ExtraType.Sample,
+ Model.Entities.ExtraType.Scene
+ };
+
+ private string _sortName;
+
+ private string _forcedSortName;
+
+ private string _name;
+
+ public const char SlugChar = '-';
+
protected BaseItem()
{
Tags = Array.Empty<string>();
@@ -73,71 +98,6 @@ namespace MediaBrowser.Controller.Entities
ExtraIds = Array.Empty<Guid>();
}
- public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
- public static char SlugChar = '-';
-
- /// <summary>
- /// The trailer folder name.
- /// </summary>
- public const string TrailerFolderName = "trailers";
- public const string ThemeSongsFolderName = "theme-music";
- public const string ThemeSongFilename = "theme";
- public const string ThemeVideosFolderName = "backdrops";
- public const string ExtrasFolderName = "extras";
- public const string BehindTheScenesFolderName = "behind the scenes";
- public const string DeletedScenesFolderName = "deleted scenes";
- public const string InterviewFolderName = "interviews";
- public const string SceneFolderName = "scenes";
- public const string SampleFolderName = "samples";
- public const string ShortsFolderName = "shorts";
- public const string FeaturettesFolderName = "featurettes";
-
- public static readonly string[] AllExtrasTypesFolderNames =
- {
- ExtrasFolderName,
- BehindTheScenesFolderName,
- DeletedScenesFolderName,
- InterviewFolderName,
- SceneFolderName,
- SampleFolderName,
- ShortsFolderName,
- FeaturettesFolderName
- };
-
- [JsonIgnore]
- public Guid[] ThemeSongIds
- {
- get
- {
- return _themeSongIds ??= GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeSong)
- .Select(song => song.Id)
- .ToArray();
- }
-
- private set
- {
- _themeSongIds = value;
- }
- }
-
- [JsonIgnore]
- public Guid[] ThemeVideoIds
- {
- get
- {
- return _themeVideoIds ??= GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.ThemeVideo)
- .Select(song => song.Id)
- .ToArray();
- }
-
- private set
- {
- _themeVideoIds = value;
- }
- }
-
[JsonIgnore]
public string PreferredMetadataCountryCode { get; set; }
@@ -193,8 +153,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool SupportsRemoteImageDownloading => true;
- private string _name;
-
/// <summary>
/// Gets or sets the name.
/// </summary>
@@ -317,22 +275,9 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public string ExternalSeriesId { get; set; }
- /// <summary>
- /// Gets or sets the etag.
- /// </summary>
- /// <value>The etag.</value>
- [JsonIgnore]
- public string ExternalEtag { get; set; }
-
[JsonIgnore]
public virtual bool IsHidden => false;
- public BaseItem GetOwner()
- {
- var ownerId = OwnerId;
- return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId);
- }
-
/// <summary>
/// Gets the type of the location.
/// </summary>
@@ -342,11 +287,6 @@ namespace MediaBrowser.Controller.Entities
{
get
{
- // if (IsOffline)
- // {
- // return LocationType.Offline;
- // }
-
var path = Path;
if (string.IsNullOrEmpty(path))
{
@@ -378,15 +318,8 @@ namespace MediaBrowser.Controller.Entities
}
}
- public bool IsPathProtocol(MediaProtocol protocol)
- {
- var current = PathProtocol;
-
- return current.HasValue && current.Value == protocol;
- }
-
[JsonIgnore]
- public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File);
+ public bool IsFileProtocol => PathProtocol == MediaProtocol.File;
[JsonIgnore]
public bool HasPathProtocol => PathProtocol.HasValue;
@@ -422,35 +355,17 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool EnableAlphaNumericSorting => true;
- private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
- {
- var list = new List<Tuple<StringBuilder, bool>>();
-
- int thisMarker = 0;
-
- while (thisMarker < s1.Length)
- {
- char thisCh = s1[thisMarker];
+ public virtual bool IsHD => Height >= 720;
- var thisChunk = new StringBuilder();
- bool isNumeric = char.IsDigit(thisCh);
+ public bool IsShortcut { get; set; }
- while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric)
- {
- thisChunk.Append(thisCh);
- thisMarker++;
+ public string ShortcutPath { get; set; }
- if (thisMarker < s1.Length)
- {
- thisCh = s1[thisMarker];
- }
- }
+ public int Width { get; set; }
- list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
- }
+ public int Height { get; set; }
- return list;
- }
+ public Guid[] ExtraIds { get; set; }
/// <summary>
/// Gets the primary image path.
@@ -462,72 +377,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public string PrimaryImagePath => this.GetImagePath(ImageType.Primary);
- public virtual bool CanDelete()
- {
- if (SourceType == SourceType.Channel)
- {
- return ChannelManager.CanDelete(this);
- }
-
- return IsFileProtocol;
- }
-
- public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
- {
- if (user.HasPermission(PermissionKind.EnableContentDeletion))
- {
- return true;
- }
-
- var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
-
- if (SourceType == SourceType.Channel)
- {
- return allowed.Contains(ChannelId);
- }
- else
- {
- var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
-
- foreach (var folder in collectionFolders)
- {
- if (allowed.Contains(folder.Id))
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- public bool CanDelete(User user, List<Folder> allCollectionFolders)
- {
- return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
- }
-
- public bool CanDelete(User user)
- {
- var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
-
- return CanDelete(user, allCollectionFolders);
- }
-
- public virtual bool CanDownload()
- {
- return false;
- }
-
- public virtual bool IsAuthorizedToDownload(User user)
- {
- return user.HasPermission(PermissionKind.EnableContentDownloading);
- }
-
- public bool CanDownload(User user)
- {
- return CanDownload() && IsAuthorizedToDownload(user);
- }
-
/// <summary>
/// Gets or sets the date created.
/// </summary>
@@ -547,38 +396,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public DateTime DateLastRefreshed { get; set; }
- /// <summary>
- /// Gets or sets the logger.
- /// </summary>
- public static ILogger<BaseItem> Logger { get; set; }
-
- public static ILibraryManager LibraryManager { get; set; }
-
- public static IServerConfigurationManager ConfigurationManager { get; set; }
-
- public static IProviderManager ProviderManager { get; set; }
-
- public static ILocalizationManager LocalizationManager { get; set; }
-
- public static IItemRepository ItemRepository { get; set; }
-
- public static IFileSystem FileSystem { get; set; }
-
- public static IUserDataManager UserDataManager { get; set; }
-
- public static IChannelManager ChannelManager { get; set; }
-
- public static IMediaSourceManager MediaSourceManager { get; set; }
-
- /// <summary>
- /// Returns a <see cref="string" /> that represents this instance.
- /// </summary>
- /// <returns>A <see cref="string" /> that represents this instance.</returns>
- public override string ToString()
- {
- return Name;
- }
-
[JsonIgnore]
public bool IsLocked { get; set; }
@@ -610,7 +427,45 @@ namespace MediaBrowser.Controller.Entities
}
}
- private string _forcedSortName;
+ [JsonIgnore]
+ public bool EnableMediaSourceDisplay
+ {
+ get
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return ChannelManager.EnableMediaSourceDisplay(this);
+ }
+
+ return true;
+ }
+ }
+
+ [JsonIgnore]
+ public Guid ParentId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the logger.
+ /// </summary>
+ public static ILogger<BaseItem> Logger { get; set; }
+
+ public static ILibraryManager LibraryManager { get; set; }
+
+ public static IServerConfigurationManager ConfigurationManager { get; set; }
+
+ public static IProviderManager ProviderManager { get; set; }
+
+ public static ILocalizationManager LocalizationManager { get; set; }
+
+ public static IItemRepository ItemRepository { get; set; }
+
+ public static IFileSystem FileSystem { get; set; }
+
+ public static IUserDataManager UserDataManager { get; set; }
+
+ public static IChannelManager ChannelManager { get; set; }
+
+ public static IMediaSourceManager MediaSourceManager { get; set; }
/// <summary>
/// Gets or sets the name of the forced sort.
@@ -627,10 +482,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- private string _sortName;
- private Guid[] _themeSongIds;
- private Guid[] _themeVideoIds;
-
/// <summary>
/// Gets or sets the name of the sort.
/// </summary>
@@ -659,187 +510,8 @@ namespace MediaBrowser.Controller.Entities
set => _sortName = value;
}
- public string GetInternalMetadataPath()
- {
- var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
-
- return GetInternalMetadataPath(basePath);
- }
-
- protected virtual string GetInternalMetadataPath(string basePath)
- {
- if (SourceType == SourceType.Channel)
- {
- return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
- }
-
- ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
-
- return System.IO.Path.Join(basePath, "library", idString.Slice(0, 2), idString);
- }
-
- /// <summary>
- /// Creates the name of the sort.
- /// </summary>
- /// <returns>System.String.</returns>
- protected virtual string CreateSortName()
- {
- if (Name == null)
- {
- return null; // some items may not have name filled in properly
- }
-
- if (!EnableAlphaNumericSorting)
- {
- return Name.TrimStart();
- }
-
- var sortable = Name.Trim().ToLowerInvariant();
-
- foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
- {
- sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
- }
-
- foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
- {
- sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
- }
-
- foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
- {
- // Remove from beginning if a space follows
- if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
- {
- sortable = sortable.Remove(0, search.Length + 1);
- }
-
- // Remove from middle if surrounded by spaces
- sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
-
- // Remove from end if followed by a space
- if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
- {
- sortable = sortable.Remove(sortable.Length - (search.Length + 1));
- }
- }
-
- return ModifySortChunks(sortable);
- }
-
- private string ModifySortChunks(string name)
- {
- var chunks = GetSortChunks(name);
-
- var builder = new StringBuilder();
-
- foreach (var chunk in chunks)
- {
- var chunkBuilder = chunk.Item1;
-
- // This chunk is numeric
- if (chunk.Item2)
- {
- while (chunkBuilder.Length < 10)
- {
- chunkBuilder.Insert(0, '0');
- }
- }
-
- builder.Append(chunkBuilder);
- }
-
- // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
- return builder.ToString().RemoveDiacritics();
- }
-
[JsonIgnore]
- public bool EnableMediaSourceDisplay
- {
- get
- {
- if (SourceType == SourceType.Channel)
- {
- return ChannelManager.EnableMediaSourceDisplay(this);
- }
-
- return true;
- }
- }
-
- [JsonIgnore]
- public Guid ParentId { get; set; }
-
- /// <summary>
- /// Gets or sets the parent.
- /// </summary>
- /// <value>The parent.</value>
- [JsonIgnore]
- public Folder Parent
- {
- get => GetParent() as Folder;
- set
- {
- }
- }
-
- public void SetParent(Folder parent)
- {
- ParentId = parent == null ? Guid.Empty : parent.Id;
- }
-
- public BaseItem GetParent()
- {
- var parentId = ParentId;
- if (!parentId.Equals(Guid.Empty))
- {
- return LibraryManager.GetItemById(parentId);
- }
-
- return null;
- }
-
- public IEnumerable<BaseItem> GetParents()
- {
- var parent = GetParent();
-
- while (parent != null)
- {
- yield return parent;
-
- parent = parent.GetParent();
- }
- }
-
- /// <summary>
- /// Finds a parent of a given type.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <returns>``0.</returns>
- public T FindParent<T>()
- where T : Folder
- {
- foreach (var parent in GetParents())
- {
- var item = parent as T;
- if (item != null)
- {
- return item;
- }
- }
-
- return null;
- }
-
- [JsonIgnore]
- public virtual Guid DisplayParentId
- {
- get
- {
- var parentId = ParentId;
- return parentId;
- }
- }
+ public virtual Guid DisplayParentId => ParentId;
[JsonIgnore]
public BaseItem DisplayParent
@@ -1014,6 +686,342 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ [JsonIgnore]
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ [JsonIgnore]
+ public virtual Folder LatestItemsIndexContainer => null;
+
+ [JsonIgnore]
+ public string PresentationUniqueKey { get; set; }
+
+ [JsonIgnore]
+ public virtual bool EnableRememberingTrackSelections => true;
+
+ [JsonIgnore]
+ public virtual bool IsTopParent
+ {
+ get
+ {
+ if (this is BasePluginFolder || this is Channel)
+ {
+ return true;
+ }
+
+ if (this is IHasCollectionType view)
+ {
+ if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ if (GetParent() is AggregateFolder)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ [JsonIgnore]
+ public virtual bool SupportsAncestors => true;
+
+ [JsonIgnore]
+ public virtual bool StopRefreshIfLocalMetadataFound => true;
+
+ [JsonIgnore]
+ protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol;
+
+ [JsonIgnore]
+ public virtual bool SupportsPeople => false;
+
+ [JsonIgnore]
+ public virtual bool SupportsThemeMedia => false;
+
+ [JsonIgnore]
+ public virtual bool SupportsInheritedParentImages => false;
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public virtual bool IsFolder => false;
+
+ [JsonIgnore]
+ public virtual bool IsDisplayedAsFolder => false;
+
+ /// <summary>
+ /// Gets or sets the remote trailers.
+ /// </summary>
+ /// <value>The remote trailers.</value>
+ public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
+
+ public virtual bool SupportsExternalTransfer => false;
+
+ public virtual double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 0;
+ }
+
+ public virtual string CreatePresentationUniqueKey()
+ {
+ return Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+
+ private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
+ {
+ var list = new List<Tuple<StringBuilder, bool>>();
+
+ int thisMarker = 0;
+
+ while (thisMarker < s1.Length)
+ {
+ char thisCh = s1[thisMarker];
+
+ var thisChunk = new StringBuilder();
+ bool isNumeric = char.IsDigit(thisCh);
+
+ while (thisMarker < s1.Length && char.IsDigit(thisCh) == isNumeric)
+ {
+ thisChunk.Append(thisCh);
+ thisMarker++;
+
+ if (thisMarker < s1.Length)
+ {
+ thisCh = s1[thisMarker];
+ }
+ }
+
+ list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
+ }
+
+ return list;
+ }
+
+ public virtual bool CanDelete()
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return ChannelManager.CanDelete(this);
+ }
+
+ return IsFileProtocol;
+ }
+
+ public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
+ {
+ if (user.HasPermission(PermissionKind.EnableContentDeletion))
+ {
+ return true;
+ }
+
+ var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
+
+ if (SourceType == SourceType.Channel)
+ {
+ return allowed.Contains(ChannelId);
+ }
+ else
+ {
+ var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
+
+ foreach (var folder in collectionFolders)
+ {
+ if (allowed.Contains(folder.Id))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public BaseItem GetOwner()
+ {
+ var ownerId = OwnerId;
+ return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId);
+ }
+
+ public bool CanDelete(User user, List<Folder> allCollectionFolders)
+ {
+ return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
+ }
+
+ public bool CanDelete(User user)
+ {
+ var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
+
+ return CanDelete(user, allCollectionFolders);
+ }
+
+ public virtual bool CanDownload()
+ {
+ return false;
+ }
+
+ public virtual bool IsAuthorizedToDownload(User user)
+ {
+ return user.HasPermission(PermissionKind.EnableContentDownloading);
+ }
+
+ public bool CanDownload(User user)
+ {
+ return CanDownload() && IsAuthorizedToDownload(user);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="string" /> that represents this instance.
+ /// </summary>
+ /// <returns>A <see cref="string" /> that represents this instance.</returns>
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ public string GetInternalMetadataPath()
+ {
+ var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
+
+ return GetInternalMetadataPath(basePath);
+ }
+
+ protected virtual string GetInternalMetadataPath(string basePath)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return System.IO.Path.Join(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture));
+ }
+
+ ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture);
+
+ return System.IO.Path.Join(basePath, "library", idString[..2], idString);
+ }
+
+ /// <summary>
+ /// Creates the name of the sort.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ protected virtual string CreateSortName()
+ {
+ if (Name == null)
+ {
+ return null; // some items may not have name filled in properly
+ }
+
+ if (!EnableAlphaNumericSorting)
+ {
+ return Name.TrimStart();
+ }
+
+ var sortable = Name.Trim().ToLowerInvariant();
+
+ foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ {
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
+ }
+
+ foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ {
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
+ }
+
+ foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
+ {
+ // Remove from beginning if a space follows
+ if (sortable.StartsWith(search + " ", StringComparison.Ordinal))
+ {
+ sortable = sortable.Remove(0, search.Length + 1);
+ }
+
+ // Remove from middle if surrounded by spaces
+ sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
+
+ // Remove from end if followed by a space
+ if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
+ {
+ sortable = sortable.Remove(sortable.Length - (search.Length + 1));
+ }
+ }
+
+ return ModifySortChunks(sortable);
+ }
+
+ private string ModifySortChunks(string name)
+ {
+ var chunks = GetSortChunks(name);
+
+ var builder = new StringBuilder();
+
+ foreach (var chunk in chunks)
+ {
+ var chunkBuilder = chunk.Item1;
+
+ // This chunk is numeric
+ if (chunk.Item2)
+ {
+ while (chunkBuilder.Length < 10)
+ {
+ chunkBuilder.Insert(0, '0');
+ }
+ }
+
+ builder.Append(chunkBuilder);
+ }
+
+ // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
+ return builder.ToString().RemoveDiacritics();
+ }
+
+ public BaseItem GetParent()
+ {
+ var parentId = ParentId;
+ if (!parentId.Equals(Guid.Empty))
+ {
+ return LibraryManager.GetItemById(parentId);
+ }
+
+ return null;
+ }
+
+ public IEnumerable<BaseItem> GetParents()
+ {
+ var parent = GetParent();
+
+ while (parent != null)
+ {
+ yield return parent;
+
+ parent = parent.GetParent();
+ }
+ }
+
+ /// <summary>
+ /// Finds a parent of a given type.
+ /// </summary>
+ /// <typeparam name="T">Type of parent.</typeparam>
+ /// <returns>``0.</returns>
+ public T FindParent<T>()
+ where T : Folder
+ {
+ foreach (var parent in GetParents())
+ {
+ if (parent is T item)
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
/// Gets the play access.
/// </summary>
/// <param name="user">The user.</param>
@@ -1088,9 +1096,9 @@ namespace MediaBrowser.Controller.Entities
.ToList();
}
- protected virtual List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+ protected virtual IEnumerable<(BaseItem, MediaSourceType)> GetAllItemsForMediaSources()
{
- return new List<Tuple<BaseItem, MediaSourceType>>();
+ return Enumerable.Empty<(BaseItem, MediaSourceType)>();
}
private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
@@ -1208,8 +1216,7 @@ namespace MediaBrowser.Controller.Entities
terms.Add(item.Name);
}
- var video = item as Video;
- if (video != null)
+ if (item is Video video)
{
if (video.Video3DFormat.HasValue)
{
@@ -1244,114 +1251,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- return string.Join('/', terms.ToArray());
- }
-
- /// <summary>
- /// Loads the theme songs.
- /// </summary>
- /// <returns>List{Audio.Audio}.</returns>
- private static Audio.Audio[] LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
- {
- var files = fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => FileSystem.GetFiles(i.FullName))
- .ToList();
-
- // Support plex/xbmc convention
- files.AddRange(fileSystemChildren
- .Where(i => !i.IsDirectory && System.IO.Path.GetFileNameWithoutExtension(i.FullName.AsSpan()).Equals(ThemeSongFilename, StringComparison.OrdinalIgnoreCase)));
-
- return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
- .OfType<Audio.Audio>()
- .Select(audio =>
- {
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio;
-
- if (dbItem != null)
- {
- audio = dbItem;
- }
- else
- {
- // item is new
- audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong;
- }
-
- return audio;
-
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path).ToArray();
- }
-
- /// <summary>
- /// Loads the video backdrops.
- /// </summary>
- /// <returns>List{Video}.</returns>
- private static Video[] LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
- {
- var files = fileSystemChildren.Where(i => i.IsDirectory)
- .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => FileSystem.GetFiles(i.FullName));
-
- return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
- .OfType<Video>()
- .Select(item =>
- {
- // Try to retrieve it from the db. If we don't find it, use the resolved version
-
- if (LibraryManager.GetItemById(item.Id) is Video dbItem)
- {
- item = dbItem;
- }
- else
- {
- // item is new
- item.ExtraType = Model.Entities.ExtraType.ThemeVideo;
- }
-
- return item;
-
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path).ToArray();
- }
-
- protected virtual BaseItem[] LoadExtras(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
- {
- var extras = new List<Video>();
-
- var libraryOptions = new LibraryOptions();
- var folders = fileSystemChildren.Where(i => i.IsDirectory).ToList();
- foreach (var extraFolderName in AllExtrasTypesFolderNames)
- {
- var files = folders
- .Where(i => string.Equals(i.Name, extraFolderName, StringComparison.OrdinalIgnoreCase))
- .SelectMany(i => FileSystem.GetFiles(i.FullName));
-
- // Re-using the same instance of LibraryOptions since it looks like it's never being altered.
- extras.AddRange(LibraryManager.ResolvePaths(files, directoryService, null, libraryOptions)
- .OfType<Video>()
- .Select(item =>
- {
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- if (LibraryManager.GetItemById(item.Id) is Video dbItem)
- {
- item = dbItem;
- }
-
- // Use some hackery to get the extra type based on foldername
- item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty, StringComparison.Ordinal), true, out ExtraType extraType)
- ? extraType
- : Model.Entities.ExtraType.Unknown;
-
- return item;
-
- // Sort them so that the list can be easily compared for changes
- }).OrderBy(i => i.Path));
- }
-
- return extras.ToArray();
+ return string.Join('/', terms);
}
public Task RefreshMetadata(CancellationToken cancellationToken)
@@ -1383,21 +1283,16 @@ namespace MediaBrowser.Controller.Entities
{
try
{
- var files = IsFileProtocol ?
- GetFileSystemChildren(options.DirectoryService).ToList() :
- new List<FileSystemMetadata>();
-
- var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
- await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
-
- if (ownedItemsChanged)
+ if (IsFileProtocol)
{
- requiresSave = true;
+ requiresSave = await RefreshedOwnedItems(options, GetFileSystemChildren(options.DirectoryService).ToList(), cancellationToken).ConfigureAwait(false);
}
+
+ await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error refreshing owned items for {path}", Path ?? Name);
+ Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name);
}
}
@@ -1418,14 +1313,46 @@ namespace MediaBrowser.Controller.Entities
}
}
- [JsonIgnore]
- protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol;
+ protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
+ {
+ if (!IsVisible(user))
+ {
+ return false;
+ }
- [JsonIgnore]
- public virtual bool SupportsPeople => false;
+ if (GetParents().Any(i => !i.IsVisible(user)))
+ {
+ return false;
+ }
- [JsonIgnore]
- public virtual bool SupportsThemeMedia => false;
+ if (checkFolders)
+ {
+ var topParent = GetParents().LastOrDefault() ?? this;
+
+ if (string.IsNullOrEmpty(topParent.Path))
+ {
+ return true;
+ }
+
+ var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
+
+ if (itemCollectionFolders.Count > 0)
+ {
+ var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList();
+ if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public void SetParent(Folder parent)
+ {
+ ParentId = parent == null ? Guid.Empty : parent.Id;
+ }
/// <summary>
/// Refreshes owned items such as trailers, theme videos, special features, etc.
@@ -1437,36 +1364,12 @@ namespace MediaBrowser.Controller.Entities
/// <returns><c>true</c> if any items have changed, else <c>false</c>.</returns>
protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
- var themeSongsChanged = false;
-
- var themeVideosChanged = false;
-
- var extrasChanged = false;
-
- var localTrailersChanged = false;
-
- if (IsFileProtocol && SupportsOwnedItems)
+ if (!IsFileProtocol || !SupportsOwnedItems || IsInMixedFolder || this is ICollectionFolder or UserRootFolder or AggregateFolder || this.GetType() == typeof(Folder))
{
- if (SupportsThemeMedia)
- {
- if (!IsInMixedFolder)
- {
- themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- extrasChanged = await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
- }
- }
-
- var hasTrailers = this as IHasTrailers;
- if (hasTrailers != null)
- {
- localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
- }
+ return false;
}
- return themeSongsChanged || themeVideosChanged || extrasChanged || localTrailersChanged;
+ return await RefreshExtras(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
}
protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
@@ -1476,98 +1379,24 @@ namespace MediaBrowser.Controller.Entities
return directoryService.GetFileSystemEntries(path);
}
- private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService);
-
- var newItemIds = newItems.Select(i => i.Id);
-
- var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
- var ownerId = item.Id;
-
- var tasks = newItems.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
-
- if (i.ExtraType != Model.Entities.ExtraType.Trailer ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
- {
- i.ExtraType = Model.Entities.ExtraType.Trailer;
- i.OwnerId = ownerId;
- i.ParentId = Guid.Empty;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- item.LocalTrailerIds = newItemIds.ToArray();
-
- return itemsChanged;
- }
-
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
- var extras = LoadExtras(fileSystemChildren, options.DirectoryService);
- var themeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
- var themeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
- var newExtras = new BaseItem[extras.Length + themeVideos.Length + themeSongs.Length];
- extras.CopyTo(newExtras, 0);
- themeVideos.CopyTo(newExtras, extras.Length);
- themeSongs.CopyTo(newExtras, extras.Length + themeVideos.Length);
-
- var newExtraIds = newExtras.Select(i => i.Id).ToArray();
-
+ var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
+ var newExtraIds = extras.Select(i => i.Id).ToArray();
var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
- if (extrasChanged)
+ if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
{
- var ownerId = item.Id;
-
- var tasks = newExtras.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
- if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
- {
- i.OwnerId = ownerId;
- i.ParentId = Guid.Empty;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- item.ExtraIds = newExtraIds;
+ return false;
}
- return extrasChanged;
- }
-
- private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
-
- var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray();
-
- var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
-
var ownerId = item.Id;
- var tasks = newThemeVideos.Select(i =>
+ var tasks = extras.Select(i =>
{
var subOptions = new MetadataRefreshOptions(options);
-
- if (!i.ExtraType.HasValue ||
- i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
+ if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
- i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
@@ -1578,73 +1407,11 @@ namespace MediaBrowser.Controller.Entities
await Task.WhenAll(tasks).ConfigureAwait(false);
- // They are expected to be sorted by SortName
- item.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
+ item.ExtraIds = newExtraIds;
- return themeVideosChanged;
- }
-
- /// <summary>
- /// Refreshes the theme songs.
- /// </summary>
- private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
- var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray();
-
- var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
-
- var ownerId = item.Id;
-
- var tasks = newThemeSongs.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
-
- if (!i.ExtraType.HasValue ||
- i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
- {
- i.ExtraType = Model.Entities.ExtraType.ThemeSong;
- i.OwnerId = ownerId;
- i.ParentId = Guid.Empty;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- // They are expected to be sorted by SortName
- item.ThemeSongIds = newThemeSongs.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
-
- return themeSongsChanged;
- }
-
- /// <summary>
- /// Gets or sets the provider ids.
- /// </summary>
- /// <value>The provider ids.</value>
- [JsonIgnore]
- public Dictionary<string, string> ProviderIds { get; set; }
-
- [JsonIgnore]
- public virtual Folder LatestItemsIndexContainer => null;
-
- public virtual double GetDefaultPrimaryImageAspectRatio()
- {
- return 0;
- }
-
- public virtual string CreatePresentationUniqueKey()
- {
- return Id.ToString("N", CultureInfo.InvariantCulture);
+ return true;
}
- [JsonIgnore]
- public string PresentationUniqueKey { get; set; }
-
public string GetPresentationUniqueKey()
{
return PresentationUniqueKey ?? CreatePresentationUniqueKey();
@@ -1875,7 +1642,7 @@ namespace MediaBrowser.Controller.Entities
private bool IsVisibleViaTags(User user)
{
- if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => Tags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
@@ -1942,58 +1709,9 @@ namespace MediaBrowser.Controller.Entities
return IsVisibleStandaloneInternal(user, true);
}
- [JsonIgnore]
- public virtual bool SupportsInheritedParentImages => false;
-
- protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
- {
- if (!IsVisible(user))
- {
- return false;
- }
-
- if (GetParents().Any(i => !i.IsVisible(user)))
- {
- return false;
- }
-
- if (checkFolders)
- {
- var topParent = GetParents().LastOrDefault() ?? this;
-
- if (string.IsNullOrEmpty(topParent.Path))
- {
- return true;
- }
-
- var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
-
- if (itemCollectionFolders.Count > 0)
- {
- var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList();
- if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
- {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /// <summary>
- /// Gets a value indicating whether this instance is folder.
- /// </summary>
- /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public virtual bool IsFolder => false;
-
- [JsonIgnore]
- public virtual bool IsDisplayedAsFolder => false;
-
public virtual string GetClientTypeName()
{
- if (IsFolder && SourceType == SourceType.Channel && !(this is Channel))
+ if (IsFolder && SourceType == SourceType.Channel && this is not Channel)
{
return "ChannelFolderItem";
}
@@ -2003,7 +1721,7 @@ namespace MediaBrowser.Controller.Entities
public BaseItemKind GetBaseItemKind()
{
- return Enum.Parse<BaseItemKind>(GetClientTypeName());
+ return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
}
/// <summary>
@@ -2079,14 +1797,11 @@ namespace MediaBrowser.Controller.Entities
return null;
}
- [JsonIgnore]
- public virtual bool EnableRememberingTrackSelections => true;
-
/// <summary>
/// Adds a studio to the item.
/// </summary>
/// <param name="name">The name.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
public void AddStudio(string name)
{
if (string.IsNullOrEmpty(name))
@@ -2096,7 +1811,7 @@ namespace MediaBrowser.Controller.Entities
var current = Studios;
- if (!current.Contains(name, StringComparer.OrdinalIgnoreCase))
+ if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
{
int curLen = current.Length;
if (curLen == 0)
@@ -2122,7 +1837,7 @@ namespace MediaBrowser.Controller.Entities
/// Adds a genre to the item.
/// </summary>
/// <param name="name">The name.</param>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throwns if name is null.</exception>
public void AddGenre(string name)
{
if (string.IsNullOrEmpty(name))
@@ -2131,7 +1846,7 @@ namespace MediaBrowser.Controller.Entities
}
var genres = Genres;
- if (!genres.Contains(name, StringComparer.OrdinalIgnoreCase))
+ if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var list = genres.ToList();
list.Add(name);
@@ -2145,8 +1860,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="user">The user.</param>
/// <param name="datePlayed">The date played.</param>
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
public virtual void MarkPlayed(
User user,
DateTime? datePlayed,
@@ -2183,8 +1897,7 @@ namespace MediaBrowser.Controller.Entities
/// Marks the unplayed.
/// </summary>
/// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if user is null.</exception>
public virtual void MarkUnplayed(User user)
{
if (user == null)
@@ -2234,7 +1947,11 @@ namespace MediaBrowser.Controller.Entities
var existingImage = GetImageInfo(image.Type, index);
- if (existingImage != null)
+ if (existingImage == null)
+ {
+ AddImage(image);
+ }
+ else
{
existingImage.Path = image.Path;
existingImage.DateModified = image.DateModified;
@@ -2242,15 +1959,6 @@ namespace MediaBrowser.Controller.Entities
existingImage.Height = image.Height;
existingImage.BlurHash = image.BlurHash;
}
- else
- {
- var current = ImageInfos;
- var currentCount = current.Length;
- var newArr = new ItemImageInfo[currentCount + 1];
- current.CopyTo(newArr, 0);
- newArr[currentCount] = image;
- ImageInfos = newArr;
- }
}
public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
@@ -2264,7 +1972,7 @@ namespace MediaBrowser.Controller.Entities
if (image == null)
{
- ImageInfos = ImageInfos.Concat(new[] { GetImageInfo(file, type) }).ToArray();
+ AddImage(GetImageInfo(file, type));
}
else
{
@@ -2284,6 +1992,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="type">The type.</param>
/// <param name="index">The index.</param>
+ /// <returns>A task.</returns>
public async Task DeleteImageAsync(ImageType type, int index)
{
var info = GetImageInfo(type, index);
@@ -2307,20 +2016,32 @@ namespace MediaBrowser.Controller.Entities
public void RemoveImage(ItemImageInfo image)
{
- RemoveImages(new List<ItemImageInfo> { image });
+ RemoveImages(new[] { image });
}
- public void RemoveImages(List<ItemImageInfo> deletedImages)
+ public void RemoveImages(IEnumerable<ItemImageInfo> deletedImages)
{
ImageInfos = ImageInfos.Except(deletedImages).ToArray();
}
+ public void AddImage(ItemImageInfo image)
+ {
+ var current = ImageInfos;
+ var currentCount = current.Length;
+ var newArr = new ItemImageInfo[currentCount + 1];
+ current.CopyTo(newArr, 0);
+ newArr[currentCount] = image;
+ ImageInfos = newArr;
+ }
+
public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
=> LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken);
/// <summary>
/// Validates that images within the item are still on the filesystem.
/// </summary>
+ /// <param name="directoryService">The directory service to use.</param>
+ /// <returns><c>true</c> if the images validate, <c>false</c> if not.</returns>
public bool ValidateImages(IDirectoryService directoryService)
{
var allFiles = ImageInfos
@@ -2331,12 +2052,12 @@ namespace MediaBrowser.Controller.Entities
.ToList();
var deletedImages = ImageInfos
- .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase))
+ .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparison.OrdinalIgnoreCase))
.ToList();
if (deletedImages.Count > 0)
{
- ImageInfos = ImageInfos.Except(deletedImages).ToArray();
+ RemoveImages(deletedImages);
}
return deletedImages.Count > 0;
@@ -2348,7 +2069,6 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <returns>System.String.</returns>
- /// <exception cref="InvalidOperationException"> </exception>
/// <exception cref="ArgumentNullException">Item is null.</exception>
public string GetImagePath(ImageType imageType, int imageIndex)
=> GetImageInfo(imageType, imageIndex)?.Path;
@@ -2385,6 +2105,17 @@ namespace MediaBrowser.Controller.Entities
};
}
+ // Music albums usually don't have dedicated backdrops, so return one from the artist instead
+ if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
+ {
+ var artist = FindParent<MusicArtist>();
+
+ if (artist != null)
+ {
+ return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
+ }
+ }
+
return GetImages(imageType)
.ElementAtOrDefault(imageIndex);
}
@@ -2448,11 +2179,11 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Adds the images.
+ /// Adds the images, updating metadata if they already are part of this item.
/// </summary>
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
- /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ /// <returns><c>true</c> if images were added or updated, <c>false</c> otherwise.</returns>
/// <exception cref="ArgumentException">Cannot call AddImages with chapter images.</exception>
public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
{
@@ -2465,7 +2196,6 @@ namespace MediaBrowser.Controller.Entities
.ToList();
var newImageList = new List<FileSystemMetadata>();
- var imageAdded = false;
var imageUpdated = false;
foreach (var newImage in images)
@@ -2481,7 +2211,6 @@ namespace MediaBrowser.Controller.Entities
if (existing == null)
{
newImageList.Add(newImage);
- imageAdded = true;
}
else
{
@@ -2502,19 +2231,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (imageAdded || images.Count != existingImages.Count)
- {
- var newImagePaths = images.Select(i => i.FullName).ToList();
-
- var deleted = existingImages
- .FindAll(i => i.IsLocalFile && !newImagePaths.Contains(i.Path.AsSpan(), StringComparison.OrdinalIgnoreCase) && !File.Exists(i.Path));
-
- if (deleted.Count > 0)
- {
- ImageInfos = ImageInfos.Except(deleted).ToArray();
- }
- }
-
if (newImageList.Count > 0)
{
ImageInfos = ImageInfos.Concat(newImageList.Select(i => GetImageInfo(i, imageType))).ToArray();
@@ -2565,7 +2281,7 @@ namespace MediaBrowser.Controller.Entities
public bool AllowsMultipleImages(ImageType type)
{
- return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter;
+ return type == ImageType.Backdrop || type == ImageType.Chapter;
}
public Task SwapImagesAsync(ImageType type, int index1, int index2)
@@ -2683,7 +2399,7 @@ namespace MediaBrowser.Controller.Entities
protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
{
- if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+ if (protocol == MediaProtocol.File)
{
return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
}
@@ -2711,8 +2427,10 @@ namespace MediaBrowser.Controller.Entities
protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
- var newOptions = new MetadataRefreshOptions(options);
- newOptions.SearchResult = null;
+ var newOptions = new MetadataRefreshOptions(options)
+ {
+ SearchResult = null
+ };
var item = this;
@@ -2773,8 +2491,10 @@ namespace MediaBrowser.Controller.Entities
protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
{
- var newOptions = new MetadataRefreshOptions(options);
- newOptions.SearchResult = null;
+ var newOptions = new MetadataRefreshOptions(options)
+ {
+ SearchResult = null
+ };
var id = LibraryManager.GetNewItemId(path, typeof(Video));
@@ -2788,14 +2508,6 @@ namespace MediaBrowser.Controller.Entities
newOptions.ForceSave = true;
}
- // var parentId = Id;
- // if (!video.IsOwnedItem || video.ParentId != parentId)
- // {
- // video.IsOwnedItem = true;
- // video.ParentId = parentId;
- // newOptions.ForceSave = true;
- // }
-
if (video == null)
{
return Task.FromResult(true);
@@ -2834,39 +2546,6 @@ namespace MediaBrowser.Controller.Entities
return GetParents().FirstOrDefault(parent => parent.IsTopParent);
}
- [JsonIgnore]
- public virtual bool IsTopParent
- {
- get
- {
- if (this is BasePluginFolder || this is Channel)
- {
- return true;
- }
-
- if (this is IHasCollectionType view)
- {
- if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- if (GetParent() is AggregateFolder)
- {
- return true;
- }
-
- return false;
- }
- }
-
- [JsonIgnore]
- public virtual bool SupportsAncestors => true;
-
- [JsonIgnore]
- public virtual bool StopRefreshIfLocalMetadataFound => true;
-
public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
{
return new[] { Id };
@@ -2901,6 +2580,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Updates the official rating based on content and returns true or false indicating if it changed.
/// </summary>
+ /// <param name="children">Media children.</param>
/// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
public bool UpdateRatingToItems(IList<BaseItem> children)
{
@@ -2911,7 +2591,7 @@ namespace MediaBrowser.Controller.Entities
.Select(i => i.OfficialRating)
.Where(i => !string.IsNullOrEmpty(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .Select(i => new Tuple<string, int?>(i, LocalizationManager.GetRatingLevel(i)))
+ .Select(i => (i, LocalizationManager.GetRatingLevel(i)))
.OrderBy(i => i.Item2 ?? 1000)
.Select(i => i.Item1);
@@ -2923,23 +2603,17 @@ namespace MediaBrowser.Controller.Entities
StringComparison.OrdinalIgnoreCase);
}
- public IEnumerable<BaseItem> GetThemeSongs()
+ public IReadOnlyList<BaseItem> GetThemeSongs()
{
- return ThemeSongIds.Select(LibraryManager.GetItemById);
+ return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong).ToArray();
}
- public IEnumerable<BaseItem> GetThemeVideos()
+ public IReadOnlyList<BaseItem> GetThemeVideos()
{
- return ThemeVideoIds.Select(LibraryManager.GetItemById);
+ return GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo).ToArray();
}
/// <summary>
- /// Gets or sets the remote trailers.
- /// </summary>
- /// <value>The remote trailers.</value>
- public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
-
- /// <summary>
/// Get all extras associated with this item, sorted by <see cref="SortName"/>.
/// </summary>
/// <returns>An enumerable containing the items.</returns>
@@ -2964,51 +2638,11 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value));
}
- public IEnumerable<BaseItem> GetTrailers()
- {
- if (this is IHasTrailers)
- {
- return ((IHasTrailers)this).LocalTrailerIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName);
- }
- else
- {
- return Array.Empty<BaseItem>();
- }
- }
-
- public virtual bool IsHD => Height >= 720;
-
- public bool IsShortcut { get; set; }
-
- public string ShortcutPath { get; set; }
-
- public int Width { get; set; }
-
- public int Height { get; set; }
-
- public Guid[] ExtraIds { get; set; }
-
public virtual long GetRunTimeTicksForPlayState()
{
return RunTimeTicks ?? 0;
}
- /// <summary>
- /// Extra types that should be counted and displayed as "Special Features" in the UI.
- /// </summary>
- public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType>
- {
- Model.Entities.ExtraType.Unknown,
- Model.Entities.ExtraType.BehindTheScenes,
- Model.Entities.ExtraType.Clip,
- Model.Entities.ExtraType.DeletedScene,
- Model.Entities.ExtraType.Interview,
- Model.Entities.ExtraType.Sample,
- Model.Entities.ExtraType.Scene
- };
-
- public virtual bool SupportsExternalTransfer => false;
-
/// <inheritdoc />
public override bool Equals(object obj)
{
@@ -3016,7 +2650,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public bool Equals(BaseItem other) => object.Equals(Id, other?.Id);
+ public bool Equals(BaseItem other) => Id == other?.Id;
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Id);
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index 89ad392a4..33870e2fb 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="file">The file.</param>
public static void SetImagePath(this BaseItem item, ImageType imageType, string file)
{
- if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
+ if (file.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
item.SetImage(
new ItemImageInfo
@@ -64,6 +64,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="source">The source object.</param>
/// <param name="dest">The destination object.</param>
+ /// <typeparam name="T">Source type.</typeparam>
+ /// <typeparam name="TU">Destination type.</typeparam>
public static void DeepCopy<T, TU>(this T source, TU dest)
where T : BaseItem
where TU : BaseItem
@@ -109,6 +111,9 @@ namespace MediaBrowser.Controller.Entities
/// Copies all properties on newly created object. Skips properties that do not exist.
/// </summary>
/// <param name="source">The source object.</param>
+ /// <typeparam name="T">Source type.</typeparam>
+ /// <typeparam name="TU">Destination type.</typeparam>
+ /// <returns>Destination object.</returns>
public static TU DeepCopy<T, TU>(this T source)
where T : BaseItem
where TU : BaseItem, new()
diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
index 1bd25042f..272a37df1 100644
--- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs
+++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
@@ -15,6 +15,12 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual string CollectionType => null;
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
public override bool CanDelete()
{
return false;
@@ -24,11 +30,5 @@ namespace MediaBrowser.Controller.Entities
{
return true;
}
-
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 4a721ca44..7dc7f774d 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -10,7 +10,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -41,6 +41,23 @@ namespace MediaBrowser.Controller.Entities
PhysicalFolderIds = Array.Empty<Guid>();
}
+ /// <summary>
+ /// Gets the display preferences id.
+ /// </summary>
+ /// <remarks>
+ /// Allow different display preferences for each collection folder.
+ /// </remarks>
+ /// <value>The display prefs id.</value>
+ [JsonIgnore]
+ public override Guid DisplayPreferencesId => Id;
+
+ [JsonIgnore]
+ public override string[] PhysicalLocations => PhysicalLocationsList;
+
+ public string[] PhysicalLocationsList { get; set; }
+
+ public Guid[] PhysicalFolderIds { get; set; }
+
public static IXmlSerializer XmlSerializer { get; set; }
public static IServerApplicationHost ApplicationHost { get; set; }
@@ -63,6 +80,9 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override IEnumerable<BaseItem> Children => GetActualChildren();
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
public override bool CanDelete()
{
return false;
@@ -77,8 +97,7 @@ namespace MediaBrowser.Controller.Entities
{
try
{
- var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions;
- if (result == null)
+ if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) is not LibraryOptions result)
{
return new LibraryOptions();
}
@@ -160,23 +179,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- /// <summary>
- /// Gets the display preferences id.
- /// </summary>
- /// <remarks>
- /// Allow different display preferences for each collection folder.
- /// </remarks>
- /// <value>The display prefs id.</value>
- [JsonIgnore]
- public override Guid DisplayPreferencesId => Id;
-
- [JsonIgnore]
- public override string[] PhysicalLocations => PhysicalLocationsList;
-
- public string[] PhysicalLocationsList { get; set; }
-
- public Guid[] PhysicalFolderIds { get; set; }
-
public override bool IsSaveLocalMetadataEnabled()
{
return true;
@@ -373,8 +375,5 @@ namespace MediaBrowser.Controller.Entities
return result;
}
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
index 244cc00be..9ce8eebe3 100644
--- a/MediaBrowser.Controller/Entities/Extensions.cs
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -2,7 +2,7 @@
using System;
using System.Linq;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the trailer URL.
/// </summary>
+ /// <param name="item">Media item.</param>
+ /// <param name="url">Trailer URL.</param>
public static void AddTrailerUrl(this BaseItem item, string url)
{
if (string.IsNullOrEmpty(url))
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 541747422..55551e70e 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1721, CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -165,6 +165,8 @@ namespace MediaBrowser.Controller.Entities
}
}
+ public static ICollectionManager CollectionManager { get; set; }
+
public override bool CanDelete()
{
if (IsRoot)
@@ -206,9 +208,8 @@ namespace MediaBrowser.Controller.Entities
/// Adds the child.
/// </summary>
/// <param name="item">The item.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception>
- public void AddChild(BaseItem item, CancellationToken cancellationToken)
+ public void AddChild(BaseItem item)
{
item.SetParent(this);
@@ -232,7 +233,7 @@ namespace MediaBrowser.Controller.Entities
public override bool IsVisible(User user)
{
- if (this is ICollectionFolder && !(this is BasePluginFolder))
+ if (this is ICollectionFolder && this is not BasePluginFolder)
{
var blockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders);
if (blockedMediaFolders.Length > 0)
@@ -259,6 +260,7 @@ namespace MediaBrowser.Controller.Entities
/// Loads our children. Validation will occur externally.
/// We want this synchronous.
/// </summary>
+ /// <returns>Returns children.</returns>
protected virtual List<BaseItem> LoadChildren()
{
// logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
@@ -301,7 +303,7 @@ namespace MediaBrowser.Controller.Entities
if (dictionary.ContainsKey(id))
{
Logger.LogError(
- "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
+ "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}",
Path ?? Name,
child.Path ?? child.Name);
}
@@ -423,7 +425,7 @@ namespace MediaBrowser.Controller.Entities
{
if (item.IsFileProtocol)
{
- Logger.LogDebug("Removed item: " + item.Path);
+ Logger.LogDebug("Removed item: {Path}", item.Path);
item.SetParent(null);
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
@@ -643,6 +645,8 @@ namespace MediaBrowser.Controller.Entities
/// Get the children of this folder from the actual file system.
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
+ /// <param name="directoryService">The directory service to use for operation.</param>
+ /// <returns>Returns set of base items.</returns>
protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
var collectionType = LibraryManager.GetContentType(this);
@@ -669,7 +673,7 @@ namespace MediaBrowser.Controller.Entities
{
if (LinkedChildren.Length > 0)
{
- if (!(this is ICollectionFolder))
+ if (this is not ICollectionFolder)
{
return GetChildren(user, true).Count;
}
@@ -726,7 +730,7 @@ namespace MediaBrowser.Controller.Entities
return PostFilterAndSort(items, query, true);
}
- if (!(this is UserRootFolder) && !(this is AggregateFolder) && query.ParentId == Guid.Empty)
+ if (this is not UserRootFolder && this is not AggregateFolder && query.ParentId == Guid.Empty)
{
query.Parent = this;
}
@@ -788,7 +792,7 @@ namespace MediaBrowser.Controller.Entities
private bool RequiresPostFiltering2(InternalItemsQuery query)
{
- if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
{
Logger.LogDebug("Query requires post-filtering due to BoxSet query");
return true;
@@ -801,9 +805,9 @@ namespace MediaBrowser.Controller.Entities
{
if (LinkedChildren.Length > 0)
{
- if (!(this is ICollectionFolder))
+ if (this is not ICollectionFolder)
{
- Logger.LogDebug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name);
+ Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name);
return true;
}
}
@@ -878,7 +882,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IsPlayed.HasValue)
{
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series)))
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series))
{
Logger.LogDebug("Query requires post-filtering due to IsPlayed");
return true;
@@ -999,8 +1003,6 @@ namespace MediaBrowser.Controller.Entities
return PostFilterAndSort(items, query, true);
}
- public static ICollectionManager CollectionManager { get; set; }
-
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
{
var user = query.User;
@@ -1011,20 +1013,22 @@ namespace MediaBrowser.Controller.Entities
items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
}
+ #pragma warning disable CA1309
if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
{
- items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1);
+ items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1);
}
if (!string.IsNullOrEmpty(query.NameStartsWith))
{
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.CurrentCultureIgnoreCase));
+ items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase));
}
if (!string.IsNullOrEmpty(query.NameLessThan))
{
- items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1);
+ items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1);
}
+ #pragma warning restore CA1309
// This must be the last filter
if (!string.IsNullOrEmpty(query.AdjacentTo))
@@ -1097,7 +1101,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie))
{
param = true;
}
@@ -1385,18 +1389,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- /// <summary>
- /// Gets allowed recursive children of an item.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
- /// <returns>IEnumerable{BaseItem}.</returns>
- /// <exception cref="ArgumentNullException"></exception>
- public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
- {
- return GetRecursiveChildren(user, null);
- }
-
public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
if (user == null)
@@ -1555,7 +1547,7 @@ namespace MediaBrowser.Controller.Entities
var childOwner = child.GetOwner() ?? child;
- if (childOwner != null && !(child is IItemByName))
+ if (child is not IItemByName)
{
var childProtocol = childOwner.PathProtocol;
if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
@@ -1679,7 +1671,6 @@ namespace MediaBrowser.Controller.Entities
/// <param name="user">The user.</param>
/// <param name="datePlayed">The date played.</param>
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
- /// <returns>Task.</returns>
public override void MarkPlayed(
User user,
DateTime? datePlayed,
@@ -1721,7 +1712,6 @@ namespace MediaBrowser.Controller.Entities
/// Marks the unplayed.
/// </summary>
/// <param name="user">The user.</param>
- /// <returns>Task.</returns>
public override void MarkUnplayed(User user)
{
var itemsResult = GetItemList(new InternalItemsQuery
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index b80a5be3b..4be673237 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
+using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
@@ -66,10 +66,10 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { Id };
query.ExcludeItemTypes = new[]
{
- nameof(MusicVideo),
- nameof(Entities.Audio.Audio),
- nameof(MusicAlbum),
- nameof(MusicArtist)
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Audio,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist
};
return LibraryManager.GetItemList(query);
diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
index 2304570fd..89e494ebc 100644
--- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
index 98c3b3edf..90d9bdd2d 100644
--- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs
+++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
@@ -20,6 +20,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the media sources.
/// </summary>
+ /// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param>
+ /// <returns>A list of media sources.</returns>
List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
List<MediaStream> GetMediaStreams();
diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
deleted file mode 100644
index ae01c223e..000000000
--- a/MediaBrowser.Controller/Entities/IHasScreenshots.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace MediaBrowser.Controller.Entities
-{
- /// <summary>
- /// The item has screenshots.
- /// </summary>
- public interface IHasScreenshots
- {
- }
-}
diff --git a/MediaBrowser.Controller/Entities/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs
index bdde744a3..dca5af873 100644
--- a/MediaBrowser.Controller/Entities/IHasShares.cs
+++ b/MediaBrowser.Controller/Entities/IHasShares.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
namespace MediaBrowser.Controller.Entities
{
diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
index f317a02ff..f47d2162f 100644
--- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
+++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
@@ -10,9 +10,9 @@ namespace MediaBrowser.Controller.Entities
public interface IHasSpecialFeatures
{
/// <summary>
- /// Gets or sets the special feature ids.
+ /// Gets the special feature ids.
/// </summary>
/// <value>The special feature ids.</value>
- IReadOnlyList<Guid> SpecialFeatureIds { get; set; }
+ IReadOnlyList<Guid> SpecialFeatureIds { get; }
}
}
diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs
index 2bd9ded33..bb4a6ea94 100644
--- a/MediaBrowser.Controller/Entities/IHasTrailers.cs
+++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs
@@ -2,7 +2,6 @@
#pragma warning disable CS1591
-using System;
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
@@ -17,18 +16,10 @@ namespace MediaBrowser.Controller.Entities
IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
/// <summary>
- /// Gets or sets the local trailer ids.
+ /// Gets the local trailers.
/// </summary>
- /// <value>The local trailer ids.</value>
- IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
- /// <summary>
- /// Gets or sets the remote trailer ids.
- /// </summary>
- /// <value>The remote trailer ids.</value>
- IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
-
- Guid Id { get; set; }
+ /// <value>The local trailers.</value>
+ IReadOnlyList<BaseItem> LocalTrailers { get; }
}
/// <summary>
@@ -39,57 +30,9 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the trailer count.
/// </summary>
+ /// <param name="item">Media item.</param>
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
public static int GetTrailerCount(this IHasTrailers item)
- => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count;
-
- /// <summary>
- /// Gets the trailer ids.
- /// </summary>
- /// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
- public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item)
- {
- var localIds = item.LocalTrailerIds;
- var remoteIds = item.RemoteTrailerIds;
-
- var all = new Guid[localIds.Count + remoteIds.Count];
- var index = 0;
- foreach (var id in localIds)
- {
- all[index++] = id;
- }
-
- foreach (var id in remoteIds)
- {
- all[index++] = id;
- }
-
- return all;
- }
-
- /// <summary>
- /// Gets the trailers.
- /// </summary>
- /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns>
- public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item)
- {
- var localIds = item.LocalTrailerIds;
- var remoteIds = item.RemoteTrailerIds;
- var libraryManager = BaseItem.LibraryManager;
-
- var all = new BaseItem[localIds.Count + remoteIds.Count];
- var index = 0;
- foreach (var id in localIds)
- {
- all[index++] = libraryManager.GetItemById(id);
- }
-
- foreach (var id in remoteIds)
- {
- all[index++] = libraryManager.GetItemById(id);
- }
-
- return all;
- }
+ => item.LocalTrailers.Count + item.RemoteTrailers.Count;
}
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index ebaf5506d..f06b5c787 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1044, CA1819, CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -12,6 +12,55 @@ namespace MediaBrowser.Controller.Entities
{
public class InternalItemsQuery
{
+ public InternalItemsQuery()
+ {
+ AlbumArtistIds = Array.Empty<Guid>();
+ AlbumIds = Array.Empty<Guid>();
+ AncestorIds = Array.Empty<Guid>();
+ ArtistIds = Array.Empty<Guid>();
+ BlockUnratedItems = Array.Empty<UnratedItem>();
+ BoxSetLibraryFolders = Array.Empty<Guid>();
+ ChannelIds = Array.Empty<Guid>();
+ ContributingArtistIds = Array.Empty<Guid>();
+ DtoOptions = new DtoOptions();
+ EnableTotalRecordCount = true;
+ ExcludeArtistIds = Array.Empty<Guid>();
+ ExcludeInheritedTags = Array.Empty<string>();
+ ExcludeItemIds = Array.Empty<Guid>();
+ ExcludeItemTypes = Array.Empty<BaseItemKind>();
+ ExcludeTags = Array.Empty<string>();
+ GenreIds = Array.Empty<Guid>();
+ Genres = Array.Empty<string>();
+ GroupByPresentationUniqueKey = true;
+ ImageTypes = Array.Empty<ImageType>();
+ IncludeItemTypes = Array.Empty<BaseItemKind>();
+ ItemIds = Array.Empty<Guid>();
+ MediaTypes = Array.Empty<string>();
+ MinSimilarityScore = 20;
+ OfficialRatings = Array.Empty<string>();
+ OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
+ PersonIds = Array.Empty<Guid>();
+ PersonTypes = Array.Empty<string>();
+ PresetViews = Array.Empty<string>();
+ SeriesStatuses = Array.Empty<SeriesStatus>();
+ SourceTypes = Array.Empty<SourceType>();
+ StudioIds = Array.Empty<Guid>();
+ Tags = Array.Empty<string>();
+ TopParentIds = Array.Empty<Guid>();
+ TrailerTypes = Array.Empty<TrailerType>();
+ VideoTypes = Array.Empty<VideoType>();
+ Years = Array.Empty<int>();
+ }
+
+ public InternalItemsQuery(User? user)
+ : this()
+ {
+ if (user != null)
+ {
+ SetUser(user);
+ }
+ }
+
public bool Recursive { get; set; }
public int? StartIndex { get; set; }
@@ -38,9 +87,9 @@ namespace MediaBrowser.Controller.Entities
public string[] MediaTypes { get; set; }
- public string[] IncludeItemTypes { get; set; }
+ public BaseItemKind[] IncludeItemTypes { get; set; }
- public string[] ExcludeItemTypes { get; set; }
+ public BaseItemKind[] ExcludeItemTypes { get; set; }
public string[] ExcludeTags { get; set; }
@@ -180,29 +229,12 @@ namespace MediaBrowser.Controller.Entities
public Guid ParentId { get; set; }
- public string? ParentType { get; set; }
+ public BaseItemKind? ParentType { get; set; }
public Guid[] AncestorIds { get; set; }
public Guid[] TopParentIds { get; set; }
- public BaseItem? Parent
- {
- set
- {
- if (value == null)
- {
- ParentId = Guid.Empty;
- ParentType = null;
- }
- else
- {
- ParentId = value.Id;
- ParentType = value.GetType().Name;
- }
- }
- }
-
public string[] PresetViews { get; set; }
public TrailerType[] TrailerTypes { get; set; }
@@ -270,70 +302,21 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public bool? DisplayAlbumFolders { get; set; }
- public InternalItemsQuery()
- {
- AlbumArtistIds = Array.Empty<Guid>();
- AlbumIds = Array.Empty<Guid>();
- AncestorIds = Array.Empty<Guid>();
- ArtistIds = Array.Empty<Guid>();
- BlockUnratedItems = Array.Empty<UnratedItem>();
- BoxSetLibraryFolders = Array.Empty<Guid>();
- ChannelIds = Array.Empty<Guid>();
- ContributingArtistIds = Array.Empty<Guid>();
- DtoOptions = new DtoOptions();
- EnableTotalRecordCount = true;
- ExcludeArtistIds = Array.Empty<Guid>();
- ExcludeInheritedTags = Array.Empty<string>();
- ExcludeItemIds = Array.Empty<Guid>();
- ExcludeItemTypes = Array.Empty<string>();
- ExcludeTags = Array.Empty<string>();
- GenreIds = Array.Empty<Guid>();
- Genres = Array.Empty<string>();
- GroupByPresentationUniqueKey = true;
- ImageTypes = Array.Empty<ImageType>();
- IncludeItemTypes = Array.Empty<string>();
- ItemIds = Array.Empty<Guid>();
- MediaTypes = Array.Empty<string>();
- MinSimilarityScore = 20;
- OfficialRatings = Array.Empty<string>();
- OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
- PersonIds = Array.Empty<Guid>();
- PersonTypes = Array.Empty<string>();
- PresetViews = Array.Empty<string>();
- SeriesStatuses = Array.Empty<SeriesStatus>();
- SourceTypes = Array.Empty<SourceType>();
- StudioIds = Array.Empty<Guid>();
- Tags = Array.Empty<string>();
- TopParentIds = Array.Empty<Guid>();
- TrailerTypes = Array.Empty<TrailerType>();
- VideoTypes = Array.Empty<VideoType>();
- Years = Array.Empty<int>();
- }
-
- public InternalItemsQuery(User? user)
- : this()
- {
- if (user != null)
- {
- SetUser(user);
- }
- }
-
- public void SetUser(User user)
+ public BaseItem? Parent
{
- MaxParentalRating = user.MaxParentalAgeRating;
-
- if (MaxParentalRating.HasValue)
+ set
{
- string other = UnratedItem.Other.ToString();
- BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
- .Where(i => i != other)
- .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+ if (value == null)
+ {
+ ParentId = Guid.Empty;
+ ParentType = null;
+ }
+ else
+ {
+ ParentId = value.Id;
+ ParentType = value.GetBaseItemKind();
+ }
}
-
- ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
-
- User = user;
}
public Dictionary<string, string>? HasAnyProviderId { get; set; }
@@ -361,5 +344,22 @@ namespace MediaBrowser.Controller.Entities
public string? SearchTerm { get; set; }
public string? SeriesTimerId { get; set; }
+
+ public void SetUser(User user)
+ {
+ MaxParentalRating = user.MaxParentalAgeRating;
+
+ if (MaxParentalRating.HasValue)
+ {
+ string other = UnratedItem.Other.ToString();
+ BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+ .Where(i => i != other)
+ .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+ }
+
+ ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
+
+ User = user;
+ }
}
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 74e84288d..6b93d8d87 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1721, CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -9,7 +9,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Entities.Movies
@@ -21,10 +20,6 @@ namespace MediaBrowser.Controller.Entities.Movies
{
public BoxSet()
{
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
-
DisplayOrder = ItemSortBy.PremiereDate;
}
@@ -38,10 +33,9 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool SupportsPeople => true;
/// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the display order.
@@ -49,6 +43,30 @@ namespace MediaBrowser.Controller.Entities.Movies
/// <value>The display order.</value>
public string DisplayOrder { get; set; }
+ [JsonIgnore]
+ private bool IsLegacyBoxSet
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Path))
+ {
+ return false;
+ }
+
+ if (LinkedChildren.Length > 0)
+ {
+ return false;
+ }
+
+ return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
+ }
+ }
+
+ [JsonIgnore]
+ public override bool IsPreSorted => true;
+
+ public Guid[] LibraryFolderIds { get; set; }
+
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
@@ -83,28 +101,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return new List<BaseItem>();
}
- [JsonIgnore]
- private bool IsLegacyBoxSet
- {
- get
- {
- if (string.IsNullOrEmpty(Path))
- {
- return false;
- }
-
- if (LinkedChildren.Length > 0)
- {
- return false;
- }
-
- return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
- }
- }
-
- [JsonIgnore]
- public override bool IsPreSorted => true;
-
public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
{
return true;
@@ -191,8 +187,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return IsVisible(user);
}
- public Guid[] LibraryFolderIds { get; set; }
-
private Guid[] GetLibraryFolderIds(User user)
{
return LibraryManager.GetUserRootFolder().GetChildren(user, true)
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index b54bbf5eb..dfaf03fda 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -7,12 +7,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.Entities.Movies
@@ -22,22 +19,16 @@ namespace MediaBrowser.Controller.Entities.Movies
/// </summary>
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
{
- public Movie()
- {
- SpecialFeatureIds = Array.Empty<Guid>();
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
- }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> SpecialFeatureIds { get; set; }
-
/// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
+ public IReadOnlyList<Guid> SpecialFeatureIds => GetExtras()
+ .Where(extra => extra.ExtraType != null && extra is Video)
+ .Select(extra => extra.Id)
+ .ToArray();
/// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the name of the TMDB collection.
@@ -66,54 +57,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return 2.0 / 3;
}
- protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- // Must have a parent to have special features
- // In other words, it must be part of the Parent/Child tree
- if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder)
- {
- var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
-
- if (specialFeaturesChanged)
- {
- hasChanges = true;
- }
- }
-
- return hasChanges;
- }
-
- private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
- {
- var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList();
- var newItemIds = newItems.Select(i => i.Id).ToArray();
-
- var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
-
- var ownerId = Id;
-
- var tasks = newItems.Select(i =>
- {
- var subOptions = new MetadataRefreshOptions(options);
-
- if (i.OwnerId != ownerId)
- {
- i.OwnerId = ownerId;
- subOptions.ForceSave = true;
- }
-
- return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken);
- });
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
-
- SpecialFeatureIds = newItemIds;
-
- return itemsChanged;
- }
-
/// <inheritdoc />
public override UnratedItem GetBlockUnratedType()
{
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index 913f76d3b..045c1b89f 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -5,7 +5,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -16,6 +16,26 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
{
+ /// <summary>
+ /// Gets the folder containing the item.
+ /// If the item is a folder, it returns the folder itself.
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [JsonIgnore]
+ public override string ContainingFolderPath => Path;
+
+ /// <summary>
+ /// Gets a value indicating whether to enable alpha numeric sorting.
+ /// </summary>
+ [JsonIgnore]
+ public override bool EnableAlphaNumericSorting => false;
+
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
@@ -49,14 +69,6 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query);
}
- /// <summary>
- /// Gets the folder containing the item.
- /// If the item is a folder, it returns the folder itself.
- /// </summary>
- /// <value>The containing folder path.</value>
- [JsonIgnore]
- public override string ContainingFolderPath => Path;
-
public override bool CanDelete()
{
return false;
@@ -67,18 +79,6 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- /// <summary>
- /// Gets a value indicating whether to enable alpha numeric sorting.
- /// </summary>
- [JsonIgnore]
- public override bool EnableAlphaNumericSorting => false;
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -129,6 +129,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
+ /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
+ /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs
index fb79323f8..2b689ae7e 100644
--- a/MediaBrowser.Controller/Entities/PersonInfo.cs
+++ b/MediaBrowser.Controller/Entities/PersonInfo.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
index 3312a0e3e..ba6ce189a 100644
--- a/MediaBrowser.Controller/Entities/Photo.cs
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -36,6 +36,30 @@ namespace MediaBrowser.Controller.Entities
}
}
+ public string CameraMake { get; set; }
+
+ public string CameraModel { get; set; }
+
+ public string Software { get; set; }
+
+ public double? ExposureTime { get; set; }
+
+ public double? FocalLength { get; set; }
+
+ public ImageOrientation? Orientation { get; set; }
+
+ public double? Aperture { get; set; }
+
+ public double? ShutterSpeed { get; set; }
+
+ public double? Latitude { get; set; }
+
+ public double? Longitude { get; set; }
+
+ public double? Altitude { get; set; }
+
+ public int? IsoSpeedRating { get; set; }
+
public override bool CanDownload()
{
return true;
@@ -69,29 +93,5 @@ namespace MediaBrowser.Controller.Entities
return base.GetDefaultPrimaryImageAspectRatio();
}
-
- public string CameraMake { get; set; }
-
- public string CameraModel { get; set; }
-
- public string Software { get; set; }
-
- public double? ExposureTime { get; set; }
-
- public double? FocalLength { get; set; }
-
- public ImageOrientation? Orientation { get; set; }
-
- public double? Aperture { get; set; }
-
- public double? ShutterSpeed { get; set; }
-
- public double? Latitude { get; set; }
-
- public double? Longitude { get; set; }
-
- public double? Altitude { get; set; }
-
- public int? IsoSpeedRating { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
index 6fd0a6c6c..c8feb1c94 100644
--- a/MediaBrowser.Controller/Entities/Studio.cs
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -5,7 +5,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
@@ -15,19 +15,6 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Studio : BaseItem, IItemByName
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
- return list;
- }
-
- public override string CreatePresentationUniqueKey()
- {
- return GetUserDataKeys()[0];
- }
-
/// <summary>
/// Gets the folder containing the item.
/// If the item is a folder, it returns the folder itself.
@@ -42,6 +29,22 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool SupportsAncestors => false;
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 16;
@@ -67,9 +70,6 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemList(query);
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
@@ -105,6 +105,8 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
+ /// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
+ /// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 31c179bca..dcc752f8c 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -20,18 +20,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
{
- public Episode()
- {
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
- }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
/// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the season in which it aired.
@@ -49,12 +41,6 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The index number.</value>
public int? IndexNumberEnd { get; set; }
- public string FindSeriesSortName()
- {
- var series = Series;
- return series == null ? SeriesName : series.SortName;
- }
-
[JsonIgnore]
protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1;
@@ -76,45 +62,6 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
protected override bool EnableDefaultVideoUserDataKeys => false;
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- // hack for tv plugins
- if (SourceType == SourceType.Channel)
- {
- return 0;
- }
-
- return 16.0 / 9;
- }
-
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- var series = Series;
- if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
- {
- var seriesUserDataKeys = series.GetUserDataKeys();
- var take = seriesUserDataKeys.Count;
- if (seriesUserDataKeys.Count > 1)
- {
- take--;
- }
-
- var newList = seriesUserDataKeys.GetRange(0, take);
- var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
- for (int i = 0; i < take; i++)
- {
- newList[i] = newList[i] + suffix;
- }
-
- newList.AddRange(list);
- list = newList;
- }
-
- return list;
- }
-
/// <summary>
/// Gets the Episode's Series Instance.
/// </summary>
@@ -161,6 +108,74 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
public string SeasonName { get; set; }
+ [JsonIgnore]
+ public override bool SupportsRemoteImageDownloading
+ {
+ get
+ {
+ if (IsMissingEpisode)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ [JsonIgnore]
+ public bool IsMissingEpisode => LocationType == LocationType.Virtual;
+
+ [JsonIgnore]
+ public Guid SeasonId { get; set; }
+
+ [JsonIgnore]
+ public Guid SeriesId { get; set; }
+
+ public string FindSeriesSortName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.SortName;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ // hack for tv plugins
+ if (SourceType == SourceType.Channel)
+ {
+ return 0;
+ }
+
+ return 16.0 / 9;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var series = Series;
+ if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
+ {
+ var seriesUserDataKeys = series.GetUserDataKeys();
+ var take = seriesUserDataKeys.Count;
+ if (seriesUserDataKeys.Count > 1)
+ {
+ take--;
+ }
+
+ var newList = seriesUserDataKeys.GetRange(0, take);
+ var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
+ for (int i = 0; i < take; i++)
+ {
+ newList[i] = newList[i] + suffix;
+ }
+
+ newList.AddRange(list);
+ list = newList;
+ }
+
+ return list;
+ }
+
public string FindSeriesPresentationUniqueKey()
{
var series = Series;
@@ -242,29 +257,6 @@ namespace MediaBrowser.Controller.Entities.TV
return false;
}
- [JsonIgnore]
- public override bool SupportsRemoteImageDownloading
- {
- get
- {
- if (IsMissingEpisode)
- {
- return false;
- }
-
- return true;
- }
- }
-
- [JsonIgnore]
- public bool IsMissingEpisode => LocationType == LocationType.Virtual;
-
- [JsonIgnore]
- public Guid SeasonId { get; set; }
-
- [JsonIgnore]
- public Guid SeriesId { get; set; }
-
public Guid FindSeriesId()
{
var series = FindParent<Series>();
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index aa62bb35b..926c7b045 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -38,6 +38,50 @@ namespace MediaBrowser.Controller.Entities.TV
[JsonIgnore]
public override Guid DisplayParentId => SeriesId;
+ /// <summary>
+ /// Gets this Episode's Series Instance.
+ /// </summary>
+ /// <value>The series.</value>
+ [JsonIgnore]
+ public Series Series
+ {
+ get
+ {
+ var seriesId = SeriesId;
+ if (seriesId == Guid.Empty)
+ {
+ seriesId = FindSeriesId();
+ }
+
+ return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series);
+ }
+ }
+
+ [JsonIgnore]
+ public string SeriesPath
+ {
+ get
+ {
+ var series = Series;
+
+ if (series != null)
+ {
+ return series.Path;
+ }
+
+ return System.IO.Path.GetDirectoryName(Path);
+ }
+ }
+
+ [JsonIgnore]
+ public string SeriesPresentationUniqueKey { get; set; }
+
+ [JsonIgnore]
+ public string SeriesName { get; set; }
+
+ [JsonIgnore]
+ public Guid SeriesId { get; set; }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -80,41 +124,6 @@ namespace MediaBrowser.Controller.Entities.TV
return result;
}
- /// <summary>
- /// Gets this Episode's Series Instance.
- /// </summary>
- /// <value>The series.</value>
- [JsonIgnore]
- public Series Series
- {
- get
- {
- var seriesId = SeriesId;
- if (seriesId == Guid.Empty)
- {
- seriesId = FindSeriesId();
- }
-
- return seriesId == Guid.Empty ? null : (LibraryManager.GetItemById(seriesId) as Series);
- }
- }
-
- [JsonIgnore]
- public string SeriesPath
- {
- get
- {
- var series = Series;
-
- if (series != null)
- {
- return series.Path;
- }
-
- return System.IO.Path.GetDirectoryName(Path);
- }
- }
-
public override string CreatePresentationUniqueKey()
{
if (IndexNumber.HasValue)
@@ -157,6 +166,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Gets the episodes.
/// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="options">The options to use.</param>
+ /// <returns>Set of episodes.</returns>
public List<BaseItem> GetEpisodes(User user, DtoOptions options)
{
return GetEpisodes(Series, user, options);
@@ -193,15 +205,6 @@ namespace MediaBrowser.Controller.Entities.TV
return UnratedItem.Series;
}
- [JsonIgnore]
- public string SeriesPresentationUniqueKey { get; set; }
-
- [JsonIgnore]
- public string SeriesName { get; set; }
-
- [JsonIgnore]
- public Guid SeriesId { get; set; }
-
public string FindSeriesPresentationUniqueKey()
{
var series = Series;
@@ -241,6 +244,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary>
+ /// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 44d07b4a4..bdadc2775 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -27,9 +27,6 @@ namespace MediaBrowser.Controller.Entities.TV
{
public Series()
{
- RemoteTrailers = Array.Empty<MediaUrl>();
- LocalTrailerIds = Array.Empty<Guid>();
- RemoteTrailerIds = Array.Empty<Guid>();
AirDays = Array.Empty<DayOfWeek>();
}
@@ -53,10 +50,9 @@ namespace MediaBrowser.Controller.Entities.TV
public override bool SupportsPeople => true;
/// <inheritdoc />
- public IReadOnlyList<Guid> LocalTrailerIds { get; set; }
-
- /// <inheritdoc />
- public IReadOnlyList<Guid> RemoteTrailerIds { get; set; }
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
+ .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
+ .ToArray();
/// <summary>
/// Gets or sets the display order.
@@ -72,6 +68,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The status.</value>
public SeriesStatus? Status { get; set; }
+ [JsonIgnore]
+ public override bool StopRefreshIfLocalMetadataFound => false;
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -128,7 +127,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { nameof(Season) },
+ IncludeItemTypes = new[] { BaseItemKind.Season },
IsVirtualItem = false,
Limit = 0,
DtoOptions = new DtoOptions(false)
@@ -156,7 +155,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { nameof(Episode) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Episode };
}
query.IsVirtualItem = false;
@@ -210,7 +209,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
- query.IncludeItemTypes = new[] { nameof(Season) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Season };
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
if (user != null && !user.DisplayMissingEpisodes)
@@ -236,7 +235,7 @@ namespace MediaBrowser.Controller.Entities.TV
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season };
}
query.IsVirtualItem = false;
@@ -256,7 +255,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
@@ -293,7 +292,7 @@ namespace MediaBrowser.Controller.Entities.TV
// Refresh seasons
foreach (var item in items)
{
- if (!(item is Season))
+ if (item is not Season)
{
continue;
}
@@ -360,7 +359,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
- IncludeItemTypes = new[] { nameof(Episode) },
+ IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
@@ -394,6 +393,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Filters the episodes by season.
/// </summary>
+ /// <param name="episodes">The episodes.</param>
+ /// <param name="parentSeason">The season.</param>
+ /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
+ /// <returns>The set of episodes.</returns>
public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials)
{
var seasonNumber = parentSeason.IndexNumber;
@@ -424,6 +427,10 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Filters the episodes by season.
/// </summary>
+ /// <param name="episodes">The episodes.</param>
+ /// <param name="seasonNumber">The season.</param>
+ /// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
+ /// <returns>The set of episodes.</returns>
public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
{
if (!includeSpecials || seasonNumber < 1)
@@ -499,8 +506,5 @@ namespace MediaBrowser.Controller.Entities.TV
return list;
}
-
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs
index 2ce396daf..ec3eb0f70 100644
--- a/MediaBrowser.Controller/Entities/TagExtensions.cs
+++ b/MediaBrowser.Controller/Entities/TagExtensions.cs
@@ -2,6 +2,7 @@
using System;
using System.Linq;
+using Jellyfin.Extensions;
namespace MediaBrowser.Controller.Entities
{
@@ -16,7 +17,7 @@ namespace MediaBrowser.Controller.Entities
var current = item.Tags;
- if (!current.Contains(name, StringComparer.OrdinalIgnoreCase))
+ if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
{
if (current.Length == 0)
{
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 732b45521..1c558d419 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -23,6 +23,9 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
}
+ [JsonIgnore]
+ public override bool StopRefreshIfLocalMetadataFound => false;
+
public TrailerType[] TrailerTypes { get; set; }
public override double GetDefaultPrimaryImageAspectRatio()
@@ -97,8 +100,5 @@ namespace MediaBrowser.Controller.Entities
return list;
}
-
- [JsonIgnore]
- public override bool StopRefreshIfLocalMetadataFound => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
index 6ab2116d7..50ba9ef30 100644
--- a/MediaBrowser.Controller/Entities/UserItemData.cs
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -12,6 +12,13 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class UserItemData
{
+ public const double MinLikeValue = 6.5;
+
+ /// <summary>
+ /// The _rating.
+ /// </summary>
+ private double? _rating;
+
/// <summary>
/// Gets or sets the user id.
/// </summary>
@@ -25,11 +32,6 @@ namespace MediaBrowser.Controller.Entities
public string Key { get; set; }
/// <summary>
- /// The _rating.
- /// </summary>
- private double? _rating;
-
- /// <summary>
/// Gets or sets the users 0-10 rating.
/// </summary>
/// <value>The rating.</value>
@@ -93,8 +95,6 @@ namespace MediaBrowser.Controller.Entities
/// <value>The index of the subtitle stream.</value>
public int? SubtitleStreamIndex { get; set; }
- public const double MinLikeValue = 6.5;
-
/// <summary>
/// Gets or sets a value indicating whether the item is liked or not.
/// This should never be serialized.
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index 2b15a52f0..e547db523 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -21,22 +21,15 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class UserRootFolder : Folder
{
- private List<Guid> _childrenIds = null;
private readonly object _childIdsLock = new object();
+ private List<Guid> _childrenIds = null;
- protected override List<BaseItem> LoadChildren()
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserRootFolder"/> class.
+ /// </summary>
+ public UserRootFolder()
{
- lock (_childIdsLock)
- {
- if (_childrenIds == null)
- {
- var list = base.LoadChildren();
- _childrenIds = list.Select(i => i.Id).ToList();
- return list;
- }
-
- return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
- }
+ IsRoot = true;
}
[JsonIgnore]
@@ -45,6 +38,12 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
+ [JsonIgnore]
+ protected override bool SupportsShortcutChildren => true;
+
+ [JsonIgnore]
+ public override bool IsPreSorted => true;
+
private void ClearCache()
{
lock (_childIdsLock)
@@ -53,6 +52,21 @@ namespace MediaBrowser.Controller.Entities
}
}
+ protected override List<BaseItem> LoadChildren()
+ {
+ lock (_childIdsLock)
+ {
+ if (_childrenIds == null)
+ {
+ var list = base.LoadChildren();
+ _childrenIds = list.Select(i => i.Id).ToList();
+ return list;
+ }
+
+ return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+ }
+ }
+
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
if (query.Recursive)
@@ -74,12 +88,6 @@ namespace MediaBrowser.Controller.Entities
return GetChildren(user, true).Count;
}
- [JsonIgnore]
- protected override bool SupportsShortcutChildren => true;
-
- [JsonIgnore]
- public override bool IsPreSorted => true;
-
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList();
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index 57dc9b59b..5c9be7337 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Querying;
@@ -15,6 +16,25 @@ namespace MediaBrowser.Controller.Entities
{
public class UserView : Folder, IHasCollectionType
{
+ private static readonly string[] _viewTypesEligibleForGrouping = new string[]
+ {
+ Model.Entities.CollectionType.Movies,
+ Model.Entities.CollectionType.TvShows,
+ string.Empty
+ };
+
+ private static readonly string[] _originalFolderViewTypes = new string[]
+ {
+ Model.Entities.CollectionType.Books,
+ Model.Entities.CollectionType.MusicVideos,
+ Model.Entities.CollectionType.HomeVideos,
+ Model.Entities.CollectionType.Photos,
+ Model.Entities.CollectionType.Music,
+ Model.Entities.CollectionType.BoxSets
+ };
+
+ public static ITVSeriesManager TVSeriesManager { get; set; }
+
/// <summary>
/// Gets or sets the view type.
/// </summary>
@@ -30,13 +50,23 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public Guid? UserId { get; set; }
- public static ITVSeriesManager TVSeriesManager;
-
/// <inheritdoc />
[JsonIgnore]
public string CollectionType => ViewType;
/// <inheritdoc />
+ [JsonIgnore]
+ public override bool SupportsInheritedParentImages => false;
+
+ /// <inheritdoc />
+ [JsonIgnore]
+ public override bool SupportsPlayedStatus => false;
+
+ /// <inheritdoc />
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
+
+ /// <inheritdoc />
public override IEnumerable<Guid> GetIdsForAncestorQuery()
{
if (!DisplayParentId.Equals(Guid.Empty))
@@ -53,17 +83,13 @@ namespace MediaBrowser.Controller.Entities
}
}
- [JsonIgnore]
- public override bool SupportsInheritedParentImages => false;
-
- [JsonIgnore]
- public override bool SupportsPlayedStatus => false;
-
+ /// <inheritdoc />
public override int GetChildCount(User user)
{
return GetChildren(user, true).Count;
}
+ /// <inheritdoc />
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
var parent = this as Folder;
@@ -77,10 +103,11 @@ namespace MediaBrowser.Controller.Entities
parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
}
- return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager)
+ return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager)
.GetUserItems(parent, this, CollectionType, query);
}
+ /// <inheritdoc />
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
query ??= new InternalItemsQuery(user);
@@ -91,16 +118,19 @@ namespace MediaBrowser.Controller.Entities
return result.ToList();
}
+ /// <inheritdoc />
public override bool CanDelete()
{
return false;
}
+ /// <inheritdoc />
public override bool IsSaveLocalMetadataEnabled()
{
return true;
}
+ /// <inheritdoc />
public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
query.SetUser(user);
@@ -111,32 +141,26 @@ namespace MediaBrowser.Controller.Entities
return GetItemList(query);
}
+ /// <inheritdoc />
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return GetChildren(user, false);
}
- private static readonly string[] UserSpecificViewTypes = new string[]
- {
- Model.Entities.CollectionType.Playlists
- };
-
public static bool IsUserSpecific(Folder folder)
{
- var collectionFolder = folder as ICollectionFolder;
-
- if (collectionFolder == null)
+ if (folder is not ICollectionFolder collectionFolder)
{
return false;
}
- var supportsUserSpecific = folder as ISupportsUserSpecificView;
- if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView)
+ if (folder is ISupportsUserSpecificView supportsUserSpecific
+ && supportsUserSpecific.EnableUserSpecificView)
{
return true;
}
- return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return string.Equals(Model.Entities.CollectionType.Playlists, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase);
}
public static bool IsEligibleForGrouping(Folder folder)
@@ -145,39 +169,19 @@ namespace MediaBrowser.Controller.Entities
&& IsEligibleForGrouping(collectionFolder.CollectionType);
}
- private static string[] ViewTypesEligibleForGrouping = new string[]
- {
- Model.Entities.CollectionType.Movies,
- Model.Entities.CollectionType.TvShows,
- string.Empty
- };
-
public static bool IsEligibleForGrouping(string viewType)
{
- return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
- private static string[] OriginalFolderViewTypes = new string[]
- {
- Model.Entities.CollectionType.Books,
- Model.Entities.CollectionType.MusicVideos,
- Model.Entities.CollectionType.HomeVideos,
- Model.Entities.CollectionType.Photos,
- Model.Entities.CollectionType.Music,
- Model.Entities.CollectionType.BoxSets
- };
-
public static bool EnableOriginalFolder(string viewType)
{
- return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return _originalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService, System.Threading.CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
-
- [JsonIgnore]
- public override bool SupportsPeople => false;
}
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index add734f62..fe44f1169 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -8,8 +8,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities.Movies;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
@@ -17,8 +16,6 @@ using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using Season = MediaBrowser.Controller.Entities.TV.Season;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Controller.Entities
@@ -30,22 +27,19 @@ namespace MediaBrowser.Controller.Entities
private readonly ILogger<BaseItem> _logger;
private readonly IUserDataManager _userDataManager;
private readonly ITVSeriesManager _tvSeriesManager;
- private readonly IServerConfigurationManager _config;
public UserViewBuilder(
IUserViewManager userViewManager,
ILibraryManager libraryManager,
ILogger<BaseItem> logger,
IUserDataManager userDataManager,
- ITVSeriesManager tvSeriesManager,
- IServerConfigurationManager config)
+ ITVSeriesManager tvSeriesManager)
{
_userViewManager = userViewManager;
_libraryManager = libraryManager;
_logger = logger;
_userDataManager = userDataManager;
_tvSeriesManager = tvSeriesManager;
- _config = config;
}
public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query)
@@ -65,7 +59,7 @@ namespace MediaBrowser.Controller.Entities
switch (viewType)
{
case CollectionType.Folders:
- return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query);
+ return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query);
case CollectionType.TvShows:
return GetTvView(queryParent, user, query);
@@ -110,7 +104,7 @@ namespace MediaBrowser.Controller.Entities
return GetMovieMovies(queryParent, user, query);
case SpecialFolder.MovieCollections:
- return GetMovieCollections(queryParent, user, query);
+ return GetMovieCollections(user, query);
case SpecialFolder.TvFavoriteEpisodes:
return GetFavoriteEpisodes(queryParent, user, query);
@@ -122,7 +116,7 @@ namespace MediaBrowser.Controller.Entities
{
if (queryParent is UserView)
{
- return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query);
+ return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), query);
}
return queryParent.GetItems(query);
@@ -144,7 +138,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
}
return parent.QueryRecursive(query);
@@ -160,7 +154,7 @@ namespace MediaBrowser.Controller.Entities
GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent)
};
- return GetResult(list, parent, query);
+ return GetResult(list, query);
}
private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query)
@@ -169,7 +163,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
return _libraryManager.GetItemsResult(query);
}
@@ -180,7 +174,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Series) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Series };
return _libraryManager.GetItemsResult(query);
}
@@ -191,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { nameof(Episode) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Episode };
return _libraryManager.GetItemsResult(query);
}
@@ -202,15 +196,15 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
return _libraryManager.GetItemsResult(query);
}
- private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query)
+ private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { nameof(BoxSet) };
+ query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
query.SetUser(user);
query.Recursive = true;
@@ -224,7 +218,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -237,7 +231,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -256,7 +250,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(Movie) },
+ IncludeItemTypes = new[] { BaseItemKind.Movie },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -275,9 +269,9 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i != null)
- .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent));
+ .Select(i => GetUserViewWithName(SpecialFolder.MovieGenre, i.SortName, parent));
- return GetResult(genres, parent, query);
+ return GetResult(genres, query);
}
private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
@@ -287,7 +281,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
return _libraryManager.GetItemsResult(query);
}
@@ -303,9 +297,9 @@ namespace MediaBrowser.Controller.Entities
{
query.IncludeItemTypes = new[]
{
- nameof(Series),
- nameof(Season),
- nameof(Episode)
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.Episode
};
}
@@ -323,7 +317,7 @@ namespace MediaBrowser.Controller.Entities
GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent)
};
- return GetResult(list, parent, query);
+ return GetResult(list, query);
}
private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query)
@@ -333,7 +327,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { nameof(Episode) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Episode };
query.IsVirtualItem = false;
return ConvertToResult(_libraryManager.GetItemList(query));
@@ -364,7 +358,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { nameof(Episode) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Episode };
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -375,7 +369,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { nameof(Series) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Series };
return _libraryManager.GetItemsResult(query);
}
@@ -384,7 +378,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(Series) },
+ IncludeItemTypes = new[] { BaseItemKind.Series },
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -403,9 +397,9 @@ namespace MediaBrowser.Controller.Entities
}
})
.Where(i => i != null)
- .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent));
+ .Select(i => GetUserViewWithName(SpecialFolder.TvGenre, i.SortName, parent));
- return GetResult(genres, parent, query);
+ return GetResult(genres, query);
}
private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
@@ -415,7 +409,7 @@ namespace MediaBrowser.Controller.Entities
query.GenreIds = new[] { displayParent.Id };
query.SetUser(user);
- query.IncludeItemTypes = new[] { nameof(Series) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Series };
return _libraryManager.GetItemsResult(query);
}
@@ -432,13 +426,12 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetResult<T>(
IEnumerable<T> items,
- BaseItem queryParent,
InternalItemsQuery query)
where T : BaseItem
{
items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
- return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config);
+ return PostFilterAndSort(items, null, query, _libraryManager);
}
public static bool FilterItem(BaseItem item, InternalItemsQuery query)
@@ -448,11 +441,9 @@ namespace MediaBrowser.Controller.Entities
public static QueryResult<BaseItem> PostFilterAndSort(
IEnumerable<BaseItem> items,
- BaseItem queryParent,
int? totalRecordLimit,
InternalItemsQuery query,
- ILibraryManager libraryManager,
- IServerConfigurationManager configurationManager)
+ ILibraryManager libraryManager)
{
var user = query.User;
@@ -501,17 +492,17 @@ namespace MediaBrowser.Controller.Entities
public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager)
{
- if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase))
+ if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetBaseItemKind()))
{
return false;
}
- if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase))
+ if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetBaseItemKind()))
{
return false;
}
@@ -752,10 +743,9 @@ namespace MediaBrowser.Controller.Entities
var val = query.HasTrailer.Value;
var trailerCount = 0;
- var hasTrailers = item as IHasTrailers;
- if (hasTrailers != null)
+ if (item is IHasTrailers hasTrailers)
{
- trailerCount = hasTrailers.GetTrailerIds().Count;
+ trailerCount = hasTrailers.GetTrailerCount();
}
var ok = val ? trailerCount > 0 : trailerCount == 0;
@@ -770,7 +760,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeSong.Value;
- var themeCount = item.ThemeSongIds.Length;
+ var themeCount = item.GetThemeSongs().Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
@@ -783,7 +773,7 @@ namespace MediaBrowser.Controller.Entities
{
var filterValue = query.HasThemeVideo.Value;
- var themeCount = item.ThemeVideoIds.Length;
+ var themeCount = item.GetThemeVideos().Count;
var ok = filterValue ? themeCount > 0 : themeCount == 0;
if (!ok)
@@ -793,7 +783,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
- if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
+ if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
@@ -817,7 +807,7 @@ namespace MediaBrowser.Controller.Entities
if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id =>
{
var studioItem = libraryManager.GetItemById(id);
- return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase);
+ return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparison.OrdinalIgnoreCase);
}))
{
return false;
@@ -827,7 +817,7 @@ namespace MediaBrowser.Controller.Entities
if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id =>
{
var genreItem = libraryManager.GetItemById(id);
- return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);
+ return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparison.OrdinalIgnoreCase);
}))
{
return false;
@@ -860,7 +850,7 @@ namespace MediaBrowser.Controller.Entities
var tags = query.Tags;
if (tags.Length > 0)
{
- if (!tags.Any(v => item.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))
+ if (!tags.Any(v => item.Tags.Contains(v, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
@@ -978,7 +968,7 @@ namespace MediaBrowser.Controller.Entities
{
var folder = i as ICollectionFolder;
- return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}).ToArray();
}
@@ -987,7 +977,7 @@ namespace MediaBrowser.Controller.Entities
{
var folder = i as ICollectionFolder;
- return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}).ToArray();
}
@@ -1001,7 +991,7 @@ namespace MediaBrowser.Controller.Entities
return new BaseItem[] { parent };
}
- private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent)
+ private UserView GetUserViewWithName(string type, string sortName, BaseItem parent)
{
return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName);
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index d05b5df2f..4f7614f96 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.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.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -28,6 +29,15 @@ namespace MediaBrowser.Controller.Entities
ISupportsPlaceHolders,
IHasMediaSources
{
+ public Video()
+ {
+ AdditionalParts = Array.Empty<string>();
+ LocalAlternateVersions = Array.Empty<string>();
+ SubtitleFiles = Array.Empty<string>();
+ AudioFiles = Array.Empty<string>();
+ LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ }
+
[JsonIgnore]
public string PrimaryVersionId { get; set; }
@@ -74,30 +84,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- public void SetPrimaryVersionId(string id)
- {
- if (string.IsNullOrEmpty(id))
- {
- PrimaryVersionId = null;
- }
- else
- {
- PrimaryVersionId = id;
- }
-
- PresentationUniqueKey = CreatePresentationUniqueKey();
- }
-
- public override string CreatePresentationUniqueKey()
- {
- if (!string.IsNullOrEmpty(PrimaryVersionId))
- {
- return PrimaryVersionId;
- }
-
- return base.CreatePresentationUniqueKey();
- }
-
[JsonIgnore]
public override bool SupportsThemeMedia => true;
@@ -114,6 +100,12 @@ namespace MediaBrowser.Controller.Entities
public string[] SubtitleFiles { get; set; }
/// <summary>
+ /// Gets or sets the audio paths.
+ /// </summary>
+ /// <value>The audio paths.</value>
+ public string[] AudioFiles { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance has subtitles.
/// </summary>
/// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value>
@@ -151,24 +143,6 @@ namespace MediaBrowser.Controller.Entities
/// <value>The aspect ratio.</value>
public string AspectRatio { get; set; }
- public Video()
- {
- AdditionalParts = Array.Empty<string>();
- LocalAlternateVersions = Array.Empty<string>();
- SubtitleFiles = Array.Empty<string>();
- LinkedAlternateVersions = Array.Empty<LinkedChild>();
- }
-
- public override bool CanDownload()
- {
- if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
- {
- return false;
- }
-
- return IsFileProtocol;
- }
-
[JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
@@ -196,16 +170,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
- public IEnumerable<Guid> GetAdditionalPartIds()
- {
- return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
- }
-
- public IEnumerable<Guid> GetLocalAlternateVersionIds()
- {
- return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
- }
-
public static ILiveTvManager LiveTvManager { get; set; }
[JsonIgnore]
@@ -222,37 +186,77 @@ namespace MediaBrowser.Controller.Entities
}
}
- protected override bool IsActiveRecording()
+ [JsonIgnore]
+ public bool IsCompleteMedia
{
- return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ get
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return !Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return !IsActiveRecording();
+ }
}
- public override bool CanDelete()
+ [JsonIgnore]
+ protected virtual bool EnableDefaultVideoUserDataKeys => true;
+
+ [JsonIgnore]
+ public override string ContainingFolderPath
{
- if (IsActiveRecording())
+ get
{
- return false;
- }
+ if (IsStacked)
+ {
+ return System.IO.Path.GetDirectoryName(Path);
+ }
- return base.CanDelete();
+ if (!IsPlaceHolder)
+ {
+ if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+ {
+ return Path;
+ }
+ }
+
+ return base.ContainingFolderPath;
+ }
}
[JsonIgnore]
- public bool IsCompleteMedia
+ public override string FileNameWithoutExtension
{
get
{
- if (SourceType == SourceType.Channel)
+ if (IsFileProtocol)
{
- return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+ if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+ {
+ return System.IO.Path.GetFileName(Path);
+ }
+
+ return System.IO.Path.GetFileNameWithoutExtension(Path);
}
- return !IsActiveRecording();
+ return null;
}
}
+ /// <summary>
+ /// Gets a value indicating whether [is3 D].
+ /// </summary>
+ /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
[JsonIgnore]
- protected virtual bool EnableDefaultVideoUserDataKeys => true;
+ public bool Is3D => Video3DFormat.HasValue;
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [JsonIgnore]
+ public override string MediaType => Model.Entities.MediaType.Video;
public override List<string> GetUserDataKeys()
{
@@ -293,6 +297,65 @@ namespace MediaBrowser.Controller.Entities
return list;
}
+ public void SetPrimaryVersionId(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ PrimaryVersionId = null;
+ }
+ else
+ {
+ PrimaryVersionId = id;
+ }
+
+ PresentationUniqueKey = CreatePresentationUniqueKey();
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ if (!string.IsNullOrEmpty(PrimaryVersionId))
+ {
+ return PrimaryVersionId;
+ }
+
+ return base.CreatePresentationUniqueKey();
+ }
+
+ public override bool CanDownload()
+ {
+ if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
+ {
+ return false;
+ }
+
+ return IsFileProtocol;
+ }
+
+ protected override bool IsActiveRecording()
+ {
+ return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ }
+
+ public override bool CanDelete()
+ {
+ if (IsActiveRecording())
+ {
+ return false;
+ }
+
+ return base.CanDelete();
+ }
+
+ public IEnumerable<Guid> GetAdditionalPartIds()
+ {
+ return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+ }
+
+ public IEnumerable<Guid> GetLocalAlternateVersionIds()
+ {
+ return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+ }
+
private string GetUserDataKey(string providerId)
{
var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant();
@@ -328,47 +391,6 @@ namespace MediaBrowser.Controller.Entities
.OrderBy(i => i.SortName);
}
- [JsonIgnore]
- public override string ContainingFolderPath
- {
- get
- {
- if (IsStacked)
- {
- return System.IO.Path.GetDirectoryName(Path);
- }
-
- if (!IsPlaceHolder)
- {
- if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
- {
- return Path;
- }
- }
-
- return base.ContainingFolderPath;
- }
- }
-
- [JsonIgnore]
- public override string FileNameWithoutExtension
- {
- get
- {
- if (IsFileProtocol)
- {
- if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
- {
- return System.IO.Path.GetFileName(Path);
- }
-
- return System.IO.Path.GetFileNameWithoutExtension(Path);
- }
-
- return null;
- }
- }
-
internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
{
var updateType = base.UpdateFromResolvedItem(newItem);
@@ -397,20 +419,6 @@ namespace MediaBrowser.Controller.Entities
return updateType;
}
- /// <summary>
- /// Gets a value indicating whether [is3 D].
- /// </summary>
- /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool Is3D => Video3DFormat.HasValue;
-
- /// <summary>
- /// Gets the type of the media.
- /// </summary>
- /// <value>The type of the media.</value>
- [JsonIgnore]
- public override string MediaType => Model.Entities.MediaType.Video;
-
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
@@ -509,35 +517,35 @@ namespace MediaBrowser.Controller.Entities
}).FirstOrDefault();
}
- protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+ protected override IEnumerable<(BaseItem, MediaSourceType)> GetAllItemsForMediaSources()
{
- var list = new List<Tuple<BaseItem, MediaSourceType>>();
+ var list = new List<(BaseItem, MediaSourceType)>
+ {
+ (this, MediaSourceType.Default)
+ };
- list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default));
- list.AddRange(GetLinkedAlternateVersions().Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping)));
+ list.AddRange(GetLinkedAlternateVersions().Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
- var primary = LibraryManager.GetItemById(PrimaryVersionId) as Video;
- if (primary != null)
+ if (LibraryManager.GetItemById(PrimaryVersionId) is Video primary)
{
var existingIds = list.Select(i => i.Item1.Id).ToList();
- list.Add(new Tuple<BaseItem, MediaSourceType>(primary, MediaSourceType.Grouping));
- list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping)));
+ list.Add((primary, MediaSourceType.Grouping));
+ list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
}
}
var localAlternates = list
.SelectMany(i =>
{
- var video = i.Item1 as Video;
- return video == null ? new List<Guid>() : video.GetLocalAlternateVersionIds();
+ return i.Item1 is Video video ? video.GetLocalAlternateVersionIds() : Enumerable.Empty<Guid>();
})
.Select(LibraryManager.GetItemById)
.Where(i => i != null)
.ToList();
- list.AddRange(localAlternates.Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Default)));
+ list.AddRange(localAlternates.Select(i => (i, MediaSourceType.Default)));
return list;
}
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index f268bc939..afdaf448b 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -15,13 +15,11 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Year : BaseItem, IItemByName
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
- list.Insert(0, "Year-" + Name);
- return list;
- }
+ [JsonIgnore]
+ public override bool SupportsPeople => false;
/// <summary>
/// Gets the folder containing the item.
@@ -31,6 +29,19 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public override string ContainingFolderPath => Path;
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, "Year-" + Name);
+ return list;
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -39,14 +50,6 @@ namespace MediaBrowser.Controller.Entities
return value;
}
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
- public override bool CanDelete()
- {
- return false;
- }
-
public override bool IsSaveLocalMetadataEnabled()
{
return true;
@@ -54,9 +57,7 @@ namespace MediaBrowser.Controller.Entities
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
- var usCulture = new CultureInfo("en-US");
-
- if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out var year))
+ if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
return new List<BaseItem>();
}
@@ -76,9 +77,6 @@ namespace MediaBrowser.Controller.Entities
return null;
}
- [JsonIgnore]
- public override bool SupportsPeople => false;
-
public static string GetPath(string name)
{
return GetPath(name, true);
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
deleted file mode 100644
index f1af01345..000000000
--- a/MediaBrowser.Controller/Extensions/StringExtensions.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace MediaBrowser.Controller.Extensions
-{
- /// <summary>
- /// Class BaseExtensions.
- /// </summary>
- public static class StringExtensions
- {
- public static string RemoveDiacritics(this string text)
- {
- var chars = Normalize(text, NormalizationForm.FormD)
- .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark);
-
- return Normalize(string.Concat(chars), NormalizationForm.FormC);
- }
-
- /// <summary>
- /// Counts the number of occurrences of [needle] in the string.
- /// </summary>
- /// <param name="value">The haystack to search in.</param>
- /// <param name="needle">The character to search for.</param>
- /// <returns>The number of occurrences of the [needle] character.</returns>
- public static int Count(this ReadOnlySpan<char> value, char needle)
- {
- var count = 0;
- var length = value.Length;
- for (var i = 0; i < length; i++)
- {
- if (value[i] == needle)
- {
- count++;
- }
- }
-
- return count;
- }
-
- private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true)
- {
- if (stripStringOnFailure)
- {
- try
- {
- return text.Normalize(form);
- }
- catch (ArgumentException)
- {
- // will throw if input contains invalid unicode chars
- // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
- text = Regex.Replace(text, "([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])", string.Empty);
- return Normalize(text, form, false);
- }
- }
-
- try
- {
- return text.Normalize(form);
- }
- catch (ArgumentException)
- {
- // if it still fails, return the original text
- return text;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
index b8a0bf331..2429ac42d 100644
--- a/MediaBrowser.Controller/IO/FileData.cs
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.IO
if (string.IsNullOrEmpty(newPath))
{
// invalid shortcut - could be old or target could just be unavailable
- logger.LogWarning("Encountered invalid shortcut: " + fullName);
+ logger.LogWarning("Encountered invalid shortcut: {Path}", fullName);
continue;
}
@@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.IO
}
catch (Exception ex)
{
- logger.LogError(ex, "Error resolving shortcut from {path}", fullName);
+ logger.LogError(ex, "Error resolving shortcut from {Path}", fullName);
}
}
else if (flattenFolderDepth > 0 && isDirectory)
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 094923842..75ec5f213 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -2,8 +2,6 @@
#pragma warning disable CS1591
-using System;
-using System.Collections.Generic;
using System.Net;
using MediaBrowser.Common;
using MediaBrowser.Model.System;
@@ -16,8 +14,6 @@ namespace MediaBrowser.Controller
/// </summary>
public interface IServerApplicationHost : IApplicationHost
{
- event EventHandler HasUpdateAvailableChanged;
-
bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; }
@@ -40,61 +36,47 @@ namespace MediaBrowser.Controller
bool ListenWithHttps { get; }
/// <summary>
- /// Gets 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>
- bool HasUpdateAvailable { get; }
-
- /// <summary>
/// Gets the name of the friendly.
/// </summary>
/// <value>The name of the friendly.</value>
string FriendlyName { get; }
/// <summary>
- /// Gets the configured published server url.
- /// </summary>
- string PublishedServerUrl { get; }
-
- /// <summary>
/// Gets the system info.
/// </summary>
- /// <param name="source">The originator of the request.</param>
+ /// <param name="request">The HTTP request.</param>
/// <returns>SystemInfo.</returns>
- SystemInfo GetSystemInfo(IPAddress source);
+ SystemInfo GetSystemInfo(HttpRequest request);
- PublicSystemInfo GetPublicSystemInfo(IPAddress address);
+ PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
/// <summary>
/// Gets a URL specific for the request.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> instance.</param>
- /// <param name="port">Optional port number.</param>
/// <returns>An accessible URL.</returns>
- string GetSmartApiUrl(HttpRequest request, int? port = null);
+ string GetSmartApiUrl(HttpRequest request);
/// <summary>
/// Gets a URL specific for the request.
/// </summary>
/// <param name="remoteAddr">The remote <see cref="IPAddress"/> of the connection.</param>
- /// <param name="port">Optional port number.</param>
/// <returns>An accessible URL.</returns>
- string GetSmartApiUrl(IPAddress remoteAddr, int? port = null);
+ string GetSmartApiUrl(IPAddress remoteAddr);
/// <summary>
/// Gets a URL specific for the request.
/// </summary>
/// <param name="hostname">The hostname used in the connection.</param>
- /// <param name="port">Optional port number.</param>
/// <returns>An accessible URL.</returns>
- string GetSmartApiUrl(string hostname, int? port = null);
+ string GetSmartApiUrl(string hostname);
/// <summary>
- /// Gets a localhost URL that can be used to access the API using the loop-back IP address.
- /// over HTTP (not HTTPS).
+ /// Gets an URL that can be used to access the API over LAN.
/// </summary>
+ /// <param name="allowHttps">A value indicating whether to allow HTTPS.</param>
/// <returns>The API URL.</returns>
- string GetLoopbackHttpApiUrl();
+ string GetApiUrlForLocalAccess(bool allowHttps = true);
/// <summary>
/// Gets a local (LAN) URL that can be used to access the API.
@@ -112,15 +94,6 @@ namespace MediaBrowser.Controller
/// <returns>The API URL.</returns>
string GetLocalApiUrl(string hostname, string scheme = null, int? port = null);
- /// <summary>
- /// Open a URL in an external browser window.
- /// </summary>
- /// <param name="url">The URL to open.</param>
- /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception>
- void LaunchUrl(string url);
-
- IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
-
string ExpandVirtualPath(string path);
string ReverseVirtualPath(string path);
diff --git a/MediaBrowser.Controller/Library/IDirectStreamProvider.cs b/MediaBrowser.Controller/Library/IDirectStreamProvider.cs
new file mode 100644
index 000000000..96f8b7eba
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IDirectStreamProvider.cs
@@ -0,0 +1,19 @@
+using System.IO;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// The direct live TV stream provider.
+ /// </summary>
+ /// <remarks>
+ /// Deprecated.
+ /// </remarks>
+ public interface IDirectStreamProvider
+ {
+ /// <summary>
+ /// Gets the live stream, shared streams seek to the end of the file first.
+ /// </summary>
+ /// <returns>The stream.</returns>
+ Stream GetStream();
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 7a4ba6a24..eba92695e 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -1,12 +1,11 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Naming.Common;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -32,14 +31,39 @@ namespace MediaBrowser.Controller.Library
public interface ILibraryManager
{
/// <summary>
+ /// Occurs when [item added].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemAdded;
+
+ /// <summary>
+ /// Occurs when [item updated].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemUpdated;
+
+ /// <summary>
+ /// Occurs when [item removed].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemRemoved;
+
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ AggregateFolder RootFolder { get; }
+
+ bool IsScanRunning { get; }
+
+ /// <summary>
/// Resolves the path.
/// </summary>
/// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent.</param>
+ /// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param>
/// <returns>BaseItem.</returns>
BaseItem ResolvePath(
FileSystemMetadata fileInfo,
- Folder parent = null);
+ Folder parent = null,
+ IDirectoryService directoryService = null);
/// <summary>
/// Resolves a set of files into a list of BaseItem.
@@ -58,15 +82,9 @@ namespace MediaBrowser.Controller.Library
string collectionType = null);
/// <summary>
- /// Gets the root folder.
- /// </summary>
- /// <value>The root folder.</value>
- AggregateFolder RootFolder { get; }
-
- /// <summary>
/// Gets a Person.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the person.</param>
/// <returns>Task{Person}.</returns>
Person GetPerson(string name);
@@ -81,7 +99,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the artist.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the artist.</param>
/// <returns>Task{Artist}.</returns>
MusicArtist GetArtist(string name);
@@ -90,21 +108,21 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets a Studio.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the studio.</param>
/// <returns>Task{Studio}.</returns>
Studio GetStudio(string name);
/// <summary>
/// Gets a Genre.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the genre.</param>
/// <returns>Task{Genre}.</returns>
Genre GetGenre(string name);
/// <summary>
/// Gets the genre.
/// </summary>
- /// <param name="name">The name.</param>
+ /// <param name="name">The name of the music genre.</param>
/// <returns>Task{MusicGenre}.</returns>
MusicGenre GetMusicGenre(string name);
@@ -113,7 +131,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="value">The value.</param>
/// <returns>Task{Year}.</returns>
- /// <exception cref="ArgumentOutOfRangeException"></exception>
+ /// <exception cref="ArgumentOutOfRangeException">Throws if year is invalid.</exception>
Year GetYear(int value);
/// <summary>
@@ -205,16 +223,26 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Creates the item.
/// </summary>
+ /// <param name="item">Item to create.</param>
+ /// <param name="parent">Parent of new item.</param>
void CreateItem(BaseItem item, BaseItem parent);
/// <summary>
/// Creates the items.
/// </summary>
+ /// <param name="items">Items to create.</param>
+ /// <param name="parent">Parent of new items.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
/// <summary>
/// Updates the item.
/// </summary>
+ /// <param name="items">Items to update.</param>
+ /// <param name="parent">Parent of updated items.</param>
+ /// <param name="updateReason">Reason for update.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Returns a Task that can be awaited.</returns>
Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
/// <summary>
@@ -224,6 +252,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="parent">The parent item.</param>
/// <param name="updateReason">The update reason.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Returns a Task that can be awaited.</returns>
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
/// <summary>
@@ -233,23 +262,6 @@ namespace MediaBrowser.Controller.Library
/// <returns>BaseItem.</returns>
BaseItem RetrieveItem(Guid id);
- bool IsScanRunning { get; }
-
- /// <summary>
- /// Occurs when [item added].
- /// </summary>
- event EventHandler<ItemChangeEventArgs> ItemAdded;
-
- /// <summary>
- /// Occurs when [item updated].
- /// </summary>
- event EventHandler<ItemChangeEventArgs> ItemUpdated;
-
- /// <summary>
- /// Occurs when [item removed].
- /// </summary>
- event EventHandler<ItemChangeEventArgs> ItemRemoved;
-
/// <summary>
/// Finds the type of the collection.
/// </summary>
@@ -294,16 +306,25 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Deletes the item.
/// </summary>
+ /// <param name="item">Item to delete.</param>
+ /// <param name="options">Options to use for deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options);
/// <summary>
/// Deletes the item.
/// </summary>
+ /// <param name="item">Item to delete.</param>
+ /// <param name="options">Options to use for deletion.</param>
+ /// <param name="notifyParentItem">Notify parent of deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem);
/// <summary>
/// Deletes the item.
/// </summary>
+ /// <param name="item">Item to delete.</param>
+ /// <param name="options">Options to use for deletion.</param>
+ /// <param name="parent">Parent of item.</param>
+ /// <param name="notifyParentItem">Notify parent of deletion.</param>
void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem);
/// <summary>
@@ -314,6 +335,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="parentId">The parent identifier.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
User user,
string name,
@@ -328,6 +350,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="name">The name.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
User user,
string name,
@@ -340,6 +363,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="name">The name.</param>
/// <param name="viewType">Type of the view.</param>
/// <param name="sortName">Name of the sort.</param>
+ /// <returns>The named view.</returns>
UserView GetNamedView(
string name,
string viewType,
@@ -374,20 +398,6 @@ namespace MediaBrowser.Controller.Library
string sortName);
/// <summary>
- /// Determines whether [is video file] [the specified path].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns><c>true</c> if [is video file] [the specified path]; otherwise, <c>false</c>.</returns>
- bool IsVideoFile(string path);
-
- /// <summary>
- /// Determines whether [is audio file] [the specified path].
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns>
- bool IsAudioFile(string path);
-
- /// <summary>
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
@@ -397,6 +407,9 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Fills the missing episode numbers from path.
/// </summary>
+ /// <param name="episode">Episode to use.</param>
+ /// <param name="forceRefresh">Option to force refresh of episode numbers.</param>
+ /// <returns>True if successful.</returns>
bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh);
/// <summary>
@@ -415,28 +428,13 @@ namespace MediaBrowser.Controller.Library
Guid GetNewItemId(string key, Type type);
/// <summary>
- /// Finds the trailers.
- /// </summary>
- /// <param name="owner">The owner.</param>
- /// <param name="fileSystemChildren">The file system children.</param>
- /// <param name="directoryService">The directory service.</param>
- /// <returns>IEnumerable&lt;Trailer&gt;.</returns>
- IEnumerable<Video> FindTrailers(
- BaseItem owner,
- List<FileSystemMetadata> fileSystemChildren,
- IDirectoryService directoryService);
-
- /// <summary>
/// Finds the extras.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="fileSystemChildren">The file system children.</param>
- /// <param name="directoryService">The directory service.</param>
- /// <returns>IEnumerable&lt;Video&gt;.</returns>
- IEnumerable<Video> FindExtras(
- BaseItem owner,
- List<FileSystemMetadata> fileSystemChildren,
- IDirectoryService directoryService);
+ /// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param>
+ /// <returns>IEnumerable&lt;BaseItem&gt;.</returns>
+ IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService);
/// <summary>
/// Gets the collection folders.
@@ -539,6 +537,9 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the items.
/// </summary>
+ /// <param name="query">The query to use.</param>
+ /// <param name="parents">Items to use for query.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
/// <summary>
@@ -566,11 +567,11 @@ namespace MediaBrowser.Controller.Library
Task RemoveVirtualFolder(string name, bool refreshLibrary);
- void AddMediaPath(string virtualFolderName, MediaPathInfo path);
+ void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath);
- void UpdateMediaPath(string virtualFolderName, MediaPathInfo path);
+ void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath);
- void RemoveMediaPath(string virtualFolderName, string path);
+ void RemoveMediaPath(string virtualFolderName, string mediaPath);
QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query);
@@ -596,11 +597,5 @@ namespace MediaBrowser.Controller.Library
BaseItem GetParentItem(string parentId, Guid? userId);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
-
- /// <summary>
- /// Gets or creates a static instance of <see cref="NamingOptions"/>.
- /// </summary>
- /// <returns>An instance of the <see cref="NamingOptions"/> class.</returns>
- NamingOptions GetNamingOptions();
}
}
diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs
index 85d866de5..4c44a17fd 100644
--- a/MediaBrowser.Controller/Library/ILiveStream.cs
+++ b/MediaBrowser.Controller/Library/ILiveStream.cs
@@ -1,7 +1,8 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1711, CS1591
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Dto;
@@ -25,5 +26,7 @@ namespace MediaBrowser.Controller.Library
Task Open(CancellationToken openCancellationToken);
Task Close();
+
+ Stream GetStream();
}
}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index d3d85a056..f1758a9d8 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -1,10 +1,9 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
-using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
@@ -34,13 +33,6 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the media streams.
/// </summary>
- /// <param name="mediaSourceId">The media source identifier.</param>
- /// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
- List<MediaStream> GetMediaStreams(string mediaSourceId);
-
- /// <summary>
- /// Gets the media streams.
- /// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
List<MediaStream> GetMediaStreams(MediaStreamQuery query);
@@ -62,16 +54,32 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the playack media sources.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="user">User to use for operation.</param>
+ /// <param name="allowMediaProbe">Option to allow media probe.</param>
+ /// <param name="enablePathSubstitution">Option to enable path substitution.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>List of media sources wrapped in an awaitable task.</returns>
Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
/// Gets the static media sources.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="enablePathSubstitution">Option to enable path substitution.</param>
+ /// <param name="user">User to use for operation.</param>
+ /// <returns>List of media sources.</returns>
List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null);
/// <summary>
/// Gets the static media source.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="mediaSourceId">Media source to get.</param>
+ /// <param name="liveStreamId">Live stream to use.</param>
+ /// <param name="enablePathSubstitution">Option to enable path substitution.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>The static media source wrapped in an awaitable task.</returns>
Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
@@ -95,6 +103,20 @@ namespace MediaBrowser.Controller.Library
Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
/// <summary>
+ /// Gets the live stream info.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>An instance of <see cref="ILiveStream"/>.</returns>
+ public ILiveStream GetLiveStreamInfo(string id);
+
+ /// <summary>
+ /// Gets the live stream info using the stream's unique id.
+ /// </summary>
+ /// <param name="uniqueId">The unique identifier.</param>
+ /// <returns>An instance of <see cref="ILiveStream"/>.</returns>
+ public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId);
+
+ /// <summary>
/// Closes the media source.
/// </summary>
/// <param name="id">The live stream identifier.</param>
@@ -110,14 +132,5 @@ namespace MediaBrowser.Controller.Library
void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
-
- Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken);
- }
-
- public interface IDirectStreamProvider
- {
- Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
-
- string GetFilePath();
}
}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
index 5bf4acebb..ca4b53fbe 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
using System.Threading;
@@ -21,6 +21,10 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Opens the media source.
/// </summary>
+ /// <param name="openToken">Token to use.</param>
+ /// <param name="currentLiveStreams">List of live streams.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>The media source wrapped as an awaitable task.</returns>
Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs
index 5fbfad881..d963fd249 100644
--- a/MediaBrowser.Controller/Library/IMetadataSaver.cs
+++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs
@@ -29,7 +29,6 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
void Save(BaseItem item, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index 5329841bf..ec34a868b 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CS1591
using System.Collections.Generic;
using Jellyfin.Data.Entities;
@@ -15,16 +15,28 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the instant mix from song.
/// </summary>
+ /// <param name="item">The item to use.</param>
+ /// <param name="user">The user to use.</param>
+ /// <param name="dtoOptions">The options to use.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from artist.
/// </summary>
+ /// <param name="artist">The artist to use.</param>
+ /// <param name="user">The user to use.</param>
+ /// <param name="dtoOptions">The options to use.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from genre.
/// </summary>
+ /// <param name="genres">The genres to use.</param>
+ /// <param name="user">The user to use.</param>
+ /// <param name="dtoOptions">The options to use.</param>
+ /// <returns>List of items.</returns>
List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index e5dcfcff0..034c40591 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1707, CS1591
using System;
using System.Collections.Generic;
@@ -42,9 +42,12 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the user data dto.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="user">User to use.</param>
+ /// <returns>User data dto.</returns>
UserItemDataDto GetUserDataDto(BaseItem item, User user);
- UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options);
+ UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options);
/// <summary>
/// Get all user data for the given user.
@@ -64,6 +67,10 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Updates playstate for an item and returns true or false indicating if it was played to completion.
/// </summary>
- bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks);
+ /// <param name="item">Item to update.</param>
+ /// <param name="data">Data to update.</param>
+ /// <param name="reportedPositionTicks">New playstate.</param>
+ /// <returns>True if playstate was updated.</returns>
+ bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 1801b1c41..993e3e18f 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -38,6 +38,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Initializes the user manager and ensures that a user exists.
/// </summary>
+ /// <returns>Awaitable task.</returns>
Task InitializeAsync();
/// <summary>
@@ -71,14 +72,6 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user.</param>
/// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
/// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
- void UpdateUser(User user);
-
- /// <summary>
- /// Updates the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <exception cref="ArgumentNullException">If user is <c>null</c>.</exception>
- /// <exception cref="ArgumentException">If the provided user doesn't exist.</exception>
/// <returns>A task representing the update of the user.</returns>
Task UpdateUserAsync(User user);
@@ -110,17 +103,24 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="user">The user.</param>
/// <returns>Task.</returns>
- void ResetEasyPassword(User user);
+ Task ResetEasyPassword(User user);
/// <summary>
/// Changes the password.
/// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="newPassword">New password to use.</param>
+ /// <returns>Awaitable task.</returns>
Task ChangePassword(User user, string newPassword);
/// <summary>
/// Changes the easy password.
/// </summary>
- void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
+ /// <param name="user">The user.</param>
+ /// <param name="newPassword">New password to use.</param>
+ /// <param name="newPasswordSha1">Hash of new password.</param>
+ /// <returns>Task.</returns>
+ Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
/// <summary>
/// Gets the user dto.
@@ -133,6 +133,12 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Authenticates the user.
/// </summary>
+ /// <param name="username">The user.</param>
+ /// <param name="password">The password to use.</param>
+ /// <param name="passwordSha1">Hash of password.</param>
+ /// <param name="remoteEndPoint">Remove endpoint to use.</param>
+ /// <param name="isUserSession">Specifies if a user session.</param>
+ /// <returns>User wrapped in awaitable task.</returns>
Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
/// <summary>
@@ -157,7 +163,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// This method updates the user's configuration.
/// This is only included as a stopgap until the new API, using this internally is not recommended.
- /// Instead, modify the user object directly, then call <see cref="UpdateUser"/>.
+ /// Instead, modify the user object directly, then call <see cref="UpdateUserAsync"/>.
/// </summary>
/// <param name="userId">The user's Id.</param>
/// <param name="config">The request containing the new user configuration.</param>
@@ -167,7 +173,7 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// This method updates the user's policy.
/// This is only included as a stopgap until the new API, using this internally is not recommended.
- /// Instead, modify the user object directly, then call <see cref="UpdateUser"/>.
+ /// Instead, modify the user object directly, then call <see cref="UpdateUserAsync"/>.
/// </summary>
/// <param name="userId">The user's Id.</param>
/// <param name="policy">The request containing the new user policy.</param>
diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs
index 46004e42f..055627d3e 100644
--- a/MediaBrowser.Controller/Library/IUserViewManager.cs
+++ b/MediaBrowser.Controller/Library/IUserViewManager.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
@@ -13,10 +13,29 @@ namespace MediaBrowser.Controller.Library
{
public interface IUserViewManager
{
+ /// <summary>
+ /// Gets user views.
+ /// </summary>
+ /// <param name="query">Query to use.</param>
+ /// <returns>Set of folders.</returns>
Folder[] GetUserViews(UserViewQuery query);
+ /// <summary>
+ /// Gets user sub views.
+ /// </summary>
+ /// <param name="parentId">Parent to use.</param>
+ /// <param name="type">Type to use.</param>
+ /// <param name="localizationKey">Localization key to use.</param>
+ /// <param name="sortName">Sort to use.</param>
+ /// <returns>User view.</returns>
UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName);
+ /// <summary>
+ /// Gets latest items.
+ /// </summary>
+ /// <param name="request">Query to use.</param>
+ /// <param name="options">Options to use.</param>
+ /// <returns>Set of items.</returns>
List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options);
}
}
diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
index a37dc7af1..3586dc69d 100644
--- a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1711, CS1591
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index 521e37274..91d162b41 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1721, CA1819, CS1591
using System;
using System.Collections.Generic;
@@ -36,6 +36,7 @@ namespace MediaBrowser.Controller.Library
DirectoryService = directoryService;
}
+ // TODO remove dependencies as properties, they should be injected where it makes sense
public IDirectoryService DirectoryService { get; }
/// <summary>
@@ -109,6 +110,21 @@ namespace MediaBrowser.Controller.Library
/// <value>The additional locations.</value>
private List<string> AdditionalLocations { get; set; }
+ /// <summary>
+ /// Gets the physical locations.
+ /// </summary>
+ /// <value>The physical locations.</value>
+ public string[] PhysicalLocations
+ {
+ get
+ {
+ var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
+ return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray();
+ }
+ }
+
+ public string CollectionType { get; set; }
+
public bool HasParent<T>()
where T : Folder
{
@@ -139,6 +155,16 @@ namespace MediaBrowser.Controller.Library
}
/// <summary>
+ /// Determines whether the specified <see cref="object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The object to compare with the current object.</param>
+ /// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ItemResolveArgs);
+ }
+
+ /// <summary>
/// Adds the additional location.
/// </summary>
/// <param name="path">The path.</param>
@@ -157,19 +183,6 @@ namespace MediaBrowser.Controller.Library
// REVIEW: @bond
/// <summary>
- /// Gets the physical locations.
- /// </summary>
- /// <value>The physical locations.</value>
- public string[] PhysicalLocations
- {
- get
- {
- var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
- return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray();
- }
- }
-
- /// <summary>
/// Gets the name of the file system entry by.
/// </summary>
/// <param name="name">The name.</param>
@@ -190,7 +203,7 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="path">The path.</param>
/// <returns>FileSystemInfo.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if path is invalid.</exception>
public FileSystemMetadata GetFileSystemEntryByPath(string path)
{
if (string.IsNullOrEmpty(path))
@@ -224,16 +237,38 @@ namespace MediaBrowser.Controller.Library
return CollectionType;
}
- public string CollectionType { get; set; }
+ /// <summary>
+ /// Gets the configured content type for the path.
+ /// </summary>
+ /// <remarks>
+ /// This is subject to future refactoring as it relies on a static property in BaseItem.
+ /// </remarks>
+ /// <returns>The configured content type.</returns>
+ public string GetConfiguredContentType()
+ {
+ return BaseItem.LibraryManager.GetConfiguredContentType(Path);
+ }
/// <summary>
- /// Determines whether the specified <see cref="object" /> is equal to this instance.
+ /// Gets the file system children that do not hit the ignore file check.
/// </summary>
- /// <param name="obj">The object to compare with the current object.</param>
- /// <returns><c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
- public override bool Equals(object obj)
+ /// <remarks>
+ /// This is subject to future refactoring as it relies on a static property in BaseItem.
+ /// </remarks>
+ /// <returns>The file system children that are not ignored.</returns>
+ public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren()
{
- return Equals(obj as ItemResolveArgs);
+ var numberOfChildren = FileSystemChildren.Length;
+ for (var i = 0; i < numberOfChildren; i++)
+ {
+ var child = FileSystemChildren[i];
+ if (BaseItem.LibraryManager.IgnoreFile(child, Parent))
+ {
+ continue;
+ }
+
+ yield return child;
+ }
}
/// <summary>
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
index 29bfeca09..d2ed3465a 100644
--- a/MediaBrowser.Controller/Library/NameExtensions.cs
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using MediaBrowser.Controller.Extensions;
+using Diacritics.Extensions;
namespace MediaBrowser.Controller.Library
{
diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
index 609336ec4..76e9eb1f5 100644
--- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
+++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
index bfe433c97..4d90346f2 100644
--- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
+++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index f4dc18e11..dbd18165d 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -22,12 +22,22 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
public interface ILiveTvManager
{
+ event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+
/// <summary>
/// Gets the services.
/// </summary>
/// <value>The services.</value>
IReadOnlyList<ILiveTvService> Services { get; }
+ IListingsProvider[] ListingProviders { get; }
+
/// <summary>
/// Gets the new timer defaults asynchronous.
/// </summary>
@@ -86,6 +96,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
+ /// <returns>A recording.</returns>
QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
/// <summary>
@@ -176,11 +187,16 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Recommended programs.</returns>
QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
/// <summary>
/// Gets the recommended programs internal.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Recommended programs.</returns>
QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
/// <summary>
@@ -202,6 +218,7 @@ namespace MediaBrowser.Controller.LiveTv
/// Gets the live tv folder.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Live TV folder.</returns>
Folder GetInternalLiveTvFolder(CancellationToken cancellationToken);
/// <summary>
@@ -213,11 +230,18 @@ namespace MediaBrowser.Controller.LiveTv
/// <summary>
/// Gets the internal channels.
/// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="dtoOptions">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Internal channels.</returns>
QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken);
/// <summary>
/// Gets the channel media sources.
/// </summary>
+ /// <param name="item">Item to search for.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Channel media sources wrapped in a task.</returns>
Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken);
/// <summary>
@@ -232,6 +256,9 @@ namespace MediaBrowser.Controller.LiveTv
/// <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>
@@ -247,7 +274,7 @@ namespace MediaBrowser.Controller.LiveTv
Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
- TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, NameValuePair[] mappings, List<ChannelInfo> providerChannels);
+ TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels);
/// <summary>
/// Gets the lineups.
@@ -271,20 +298,10 @@ namespace MediaBrowser.Controller.LiveTv
Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
- IListingsProvider[] ListingProviders { get; }
-
List<NameIdPair> GetTunerHostTypes();
Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
- event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
-
- event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
-
- event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
-
- event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
-
string GetEmbyTvActiveRecordingPath(string id);
ActiveRecordingInfo GetActiveRecordingInfo(string path);
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
index 897f263f3..ce34954e3 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
@@ -70,10 +70,10 @@ namespace MediaBrowser.Controller.LiveTv
/// <summary>
/// Updates the timer asynchronous.
/// </summary>
- /// <param name="info">The information.</param>
+ /// <param name="updatedTimer">The updated timer information.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken);
+ Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken);
/// <summary>
/// Updates the series timer asynchronous.
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
index 7dced9f5e..24820abb9 100644
--- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -30,6 +30,8 @@ namespace MediaBrowser.Controller.LiveTv
/// <summary>
/// Gets the channels.
/// </summary>
+ /// <param name="enableCache">Option to enable using cache.</param>
+ /// <param name="cancellationToken">The CancellationToken for this operation.</param>
/// <returns>Task&lt;IEnumerable&lt;ChannelInfo&gt;&gt;.</returns>
Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken);
@@ -47,6 +49,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="streamId">The stream identifier.</param>
/// <param name="currentLiveStreams">The current live streams.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>Live stream wrapped in a task.</returns>
Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
/// <summary>
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index bf759bc54..e63874f21 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -5,9 +5,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -18,23 +18,6 @@ namespace MediaBrowser.Controller.LiveTv
{
public class LiveTvChannel : BaseItem, IHasMediaSources, IHasProgramAttributes
{
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName)
- {
- list.Insert(0, GetClientTypeName() + "-" + Name);
- }
-
- return list;
- }
-
- public override UnratedItem GetBlockUnratedType()
- {
- return UnratedItem.LiveTvChannel;
- }
-
[JsonIgnore]
public override bool SupportsPositionTicksResume => false;
@@ -59,6 +42,67 @@ namespace MediaBrowser.Controller.LiveTv
[JsonIgnore]
public override LocationType LocationType => LocationType.Remote;
+ [JsonIgnore]
+ public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+
+ [JsonIgnore]
+ 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>
+ [JsonIgnore]
+ 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>
+ [JsonIgnore]
+ public bool IsSeries { 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>
+ [JsonIgnore]
+ public bool IsNews { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ [JsonIgnore]
+ public bool IsKids => Tags.Contains("Kids", StringComparison.OrdinalIgnoreCase);
+
+ [JsonIgnore]
+ public bool IsRepeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ [JsonIgnore]
+ public string EpisodeTitle { get; set; }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName)
+ {
+ list.Insert(0, GetClientTypeName() + "-" + Name);
+ }
+
+ return list;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.LiveTvChannel;
+ }
+
protected override string CreateSortName()
{
if (!string.IsNullOrEmpty(Number))
@@ -72,15 +116,12 @@ namespace MediaBrowser.Controller.LiveTv
return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);
}
- [JsonIgnore]
- public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
-
public override string GetClientTypeName()
{
return "TvChannel";
}
- public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+ public IEnumerable<BaseItem> GetTaggedItems()
{
return new List<BaseItem>();
}
@@ -120,46 +161,5 @@ namespace MediaBrowser.Controller.LiveTv
{
return false;
}
-
- [JsonIgnore]
- 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>
- [JsonIgnore]
- 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>
- [JsonIgnore]
- public bool IsSeries { 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>
- [JsonIgnore]
- public bool IsNews { get; set; }
-
- /// <summary>
- /// Gets a value indicating whether this instance is kids.
- /// </summary>
- /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
- [JsonIgnore]
- public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
-
- [JsonIgnore]
- public bool IsRepeat { get; set; }
-
- /// <summary>
- /// Gets or sets the episode title.
- /// </summary>
- /// <value>The episode title.</value>
- [JsonIgnore]
- public string EpisodeTitle { get; set; }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 9d638a0bf..6c4a5ea17 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, SA1306
using System;
using System.Collections.Generic;
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -19,54 +20,14 @@ namespace MediaBrowser.Controller.LiveTv
{
public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes
{
+ private static string EmbyServiceName = "Emby";
+
public LiveTvProgram()
{
IsVirtualItem = true;
}
- public override List<string> GetUserDataKeys()
- {
- var list = base.GetUserDataKeys();
-
- if (!IsSeries)
- {
- var key = this.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(key))
- {
- list.Insert(0, key);
- }
-
- key = this.GetProviderId(MetadataProvider.Tmdb);
- if (!string.IsNullOrEmpty(key))
- {
- list.Insert(0, key);
- }
- }
- else if (!string.IsNullOrEmpty(EpisodeTitle))
- {
- var name = GetClientTypeName();
-
- list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty));
- }
-
- return list;
- }
-
- private static string EmbyServiceName = "Emby";
-
- public override double GetDefaultPrimaryImageAspectRatio()
- {
- var serviceName = ServiceName;
-
- if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase))
- {
- return 2.0 / 3;
- }
- else
- {
- return 16.0 / 9;
- }
- }
+ public string SeriesName { get; set; }
[JsonIgnore]
public override SourceType SourceType => SourceType.LiveTV;
@@ -106,7 +67,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsSports => Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase);
+ public bool IsSports => Tags.Contains("Sports", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets a value indicating whether this instance is series.
@@ -120,28 +81,28 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase);
+ public bool IsLive => Tags.Contains("Live", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets a value indicating whether this instance is news.
/// </summary>
/// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase);
+ public bool IsNews => Tags.Contains("News", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets a value indicating whether this instance is kids.
/// </summary>
/// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
+ public bool IsKids => Tags.Contains("Kids", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets a value indicating whether this instance is premiere.
/// </summary>
/// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase);
+ public bool IsPremiere => Tags.Contains("Premiere", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets the folder containing the item.
@@ -182,6 +143,66 @@ namespace MediaBrowser.Controller.LiveTv
}
}
+ [JsonIgnore]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ // Optimization
+ if (IsNews || IsSports)
+ {
+ return false;
+ }
+
+ return base.SupportsPeople;
+ }
+ }
+
+ [JsonIgnore]
+ public override bool SupportsAncestors => false;
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!IsSeries)
+ {
+ var key = this.GetProviderId(MetadataProvider.Imdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+
+ key = this.GetProviderId(MetadataProvider.Tmdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+ }
+ else if (!string.IsNullOrEmpty(EpisodeTitle))
+ {
+ var name = GetClientTypeName();
+
+ list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty));
+ }
+
+ return list;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ var serviceName = ServiceName;
+
+ if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase))
+ {
+ return 2.0 / 3;
+ }
+ else
+ {
+ return 16.0 / 9;
+ }
+ }
+
public override string GetClientTypeName()
{
return "Program";
@@ -202,24 +223,6 @@ namespace MediaBrowser.Controller.LiveTv
return false;
}
- [JsonIgnore]
- public override bool SupportsPeople
- {
- get
- {
- // Optimization
- if (IsNews || IsSports)
- {
- return false;
- }
-
- return base.SupportsPeople;
- }
- }
-
- [JsonIgnore]
- public override bool SupportsAncestors => false;
-
private LiveTvOptions GetConfiguration()
{
return ConfigurationManager.GetConfiguration<LiveTvOptions>("livetv");
@@ -273,7 +276,5 @@ namespace MediaBrowser.Controller.LiveTv
return list;
}
-
- public string SeriesName { get; set; }
}
}
diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
index 1a2e8acb3..62541ea8b 100644
--- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs
+++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Extensions;
using MediaBrowser.Model.LiveTv;
namespace MediaBrowser.Controller.LiveTv
@@ -123,11 +123,11 @@ namespace MediaBrowser.Controller.LiveTv
public bool IsMovie { get; set; }
- public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
+ public bool IsKids => Tags.Contains("Kids", StringComparison.OrdinalIgnoreCase);
- public bool IsSports => Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase);
+ public bool IsSports => Tags.Contains("Sports", StringComparison.OrdinalIgnoreCase);
- public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase);
+ public bool IsNews => Tags.Contains("News", StringComparison.OrdinalIgnoreCase);
public bool IsSeries { get; set; }
@@ -136,10 +136,10 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase);
+ public bool IsLive => Tags.Contains("Live", StringComparison.OrdinalIgnoreCase);
[JsonIgnore]
- public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase);
+ public bool IsPremiere => Tags.Contains("Premiere", StringComparison.OrdinalIgnoreCase);
public int? ProductionYear { get; set; }
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index ee76ff080..cf3b7bc7a 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,10 +14,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
- <PackageReference Include="System.Threading.Tasks.Dataflow" Version="5.0.0" />
+ <PackageReference Include="Diacritics" Version="3.3.10" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+ <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
@@ -31,13 +32,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 745ee6bdb..dd6f468da 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -10,6 +10,15 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public class BaseEncodingJobOptions
{
+ public BaseEncodingJobOptions()
+ {
+ EnableAutoStreamCopy = true;
+ AllowVideoStreamCopy = true;
+ AllowAudioStreamCopy = true;
+ Context = EncodingContext.Streaming;
+ StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
/// <summary>
/// Gets or sets the id.
/// </summary>
@@ -191,14 +200,5 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
-
- public BaseEncodingJobOptions()
- {
- EnableAutoStreamCopy = true;
- AllowVideoStreamCopy = true;
- AllowAudioStreamCopy = true;
- Context = EncodingContext.Streaming;
- StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 26b0bc3de..92b345f12 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -16,16 +15,12 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Configuration;
namespace MediaBrowser.Controller.MediaEncoding
{
public class EncodingHelper
{
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
@@ -40,6 +35,8 @@ namespace MediaBrowser.Controller.MediaEncoding
"ConstrainedHigh"
};
+ private static readonly Version _minVersionForCudaOverlay = new Version(4, 4);
+
public EncodingHelper(
IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder)
@@ -109,17 +106,41 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsCudaSupported()
{
return _mediaEncoder.SupportsHwaccel("cuda")
- && _mediaEncoder.SupportsFilter("scale_cuda", null)
- && _mediaEncoder.SupportsFilter("yadif_cuda", null);
+ && _mediaEncoder.SupportsFilter("scale_cuda")
+ && _mediaEncoder.SupportsFilter("yadif_cuda")
+ && _mediaEncoder.SupportsFilter("hwupload_cuda");
}
- private bool IsTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
+ private bool IsOpenclTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
{
var videoStream = state.VideoStream;
- return IsColorDepth10(state)
+ if (videoStream == null)
+ {
+ return false;
+ }
+
+ return options.EnableTonemapping
+ && (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+ && IsColorDepth10(state)
&& _mediaEncoder.SupportsHwaccel("opencl")
- && options.EnableTonemapping
- && string.Equals(videoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
+ && _mediaEncoder.SupportsFilter("tonemap_opencl");
+ }
+
+ private bool IsCudaTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
+ {
+ var videoStream = state.VideoStream;
+ if (videoStream == null)
+ {
+ return false;
+ }
+
+ return options.EnableTonemapping
+ && (string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+ && IsColorDepth10(state)
+ && _mediaEncoder.SupportsHwaccel("cuda")
+ && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapCudaName);
}
private bool IsVppTonemappingSupported(EncodingJobInfo state, EncodingOptions options)
@@ -135,24 +156,25 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{
// Limited to HEVC for now since the filter doesn't accept master data from VP9.
- return IsColorDepth10(state)
+ return options.EnableVppTonemapping
+ && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+ && IsColorDepth10(state)
&& string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.SupportsHwaccel("vaapi")
- && options.EnableVppTonemapping
- && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase);
+ && _mediaEncoder.SupportsFilter("tonemap_vaapi");
}
// Hybrid VPP tonemapping for QSV with VAAPI
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
- if (isLinux && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ if (OperatingSystem.IsLinux() && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
// Limited to HEVC for now since the filter doesn't accept master data from VP9.
- return IsColorDepth10(state)
+ return options.EnableVppTonemapping
+ && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
+ && IsColorDepth10(state)
&& string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.SupportsHwaccel("vaapi")
- && _mediaEncoder.SupportsHwaccel("qsv")
- && options.EnableVppTonemapping
- && string.Equals(videoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase);
+ && _mediaEncoder.SupportsFilter("tonemap_vaapi")
+ && _mediaEncoder.SupportsHwaccel("qsv");
}
// Native VPP tonemapping may come to QSV in the future.
@@ -162,6 +184,9 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the name of the output video codec.
/// </summary>
+ /// <param name="state">Encording state.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <returns>Encoder string.</returns>
public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
{
var codec = state.OutputVideoCodec;
@@ -179,11 +204,17 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetH264Encoder(state, encodingOptions);
}
- if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
{
return "libvpx";
}
+ if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libvpx-vp9";
+ }
+
if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
{
return "wmv2";
@@ -316,6 +347,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return container;
}
+ /// <summary>
+ /// Gets decoder from a codec.
+ /// </summary>
+ /// <param name="codec">Codec to use.</param>
+ /// <returns>Decoder string.</returns>
public string GetDecoderFromCodec(string codec)
{
// For these need to find out the ffmpeg names
@@ -345,6 +381,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Infers the audio codec based on the url.
/// </summary>
+ /// <param name="container">Container to use.</param>
+ /// <returns>Codec string.</returns>
public string InferAudioCodec(string container)
{
var ext = "." + (container ?? string.Empty);
@@ -408,7 +446,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
{
- return "vpx";
+ // TODO: this may not always mean VP8, as the codec ages
+ return "vp8";
}
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
@@ -490,12 +529,20 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the input argument.
/// </summary>
- public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions)
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <returns>Input arguments.</returns>
+ public string GetInputArgument(EncodingJobInfo state, EncodingOptions options)
{
var arg = new StringBuilder();
- var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
- var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+ var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
+ var outputVideoCodec = GetVideoEncoder(state, options) ?? string.Empty;
+ var isWindows = OperatingSystem.IsWindows();
+ var isLinux = OperatingSystem.IsLinux();
+ var isMacOS = OperatingSystem.IsMacOS();
+#pragma warning disable CA1508 // Defaults to string.Empty
var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+#pragma warning restore CA1508
var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
@@ -503,42 +550,40 @@ namespace MediaBrowser.Controller.MediaEncoding
var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
- var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
- var isTonemappingSupported = IsTonemappingSupported(state, encodingOptions);
- var isVppTonemappingSupported = IsVppTonemappingSupported(state, encodingOptions);
+ var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase);
+ var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+ var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+ var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
if (!IsCopyCodec(outputVideoCodec))
{
if (state.IsVideoRequest
&& _mediaEncoder.SupportsHwaccel("vaapi")
- && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{
if (isVaapiDecoder)
{
- if (isTonemappingSupported && !isVppTonemappingSupported)
+ if (isOpenclTonemappingSupported && !isVppTonemappingSupported)
{
- arg.Append("-init_hw_device vaapi=va:")
- .Append(encodingOptions.VaapiDevice)
- .Append(' ')
- .Append("-init_hw_device opencl=ocl@va ")
- .Append("-hwaccel_device va ")
- .Append("-hwaccel_output_format vaapi ")
- .Append("-filter_hw_device ocl ");
+ arg.Append("-init_hw_device vaapi=va:")
+ .Append(options.VaapiDevice)
+ .Append(" -init_hw_device opencl=ocl@va ")
+ .Append("-hwaccel_device va ")
+ .Append("-hwaccel_output_format vaapi ")
+ .Append("-filter_hw_device ocl ");
}
else
{
arg.Append("-hwaccel_output_format vaapi ")
.Append("-vaapi_device ")
- .Append(encodingOptions.VaapiDevice)
+ .Append(options.VaapiDevice)
.Append(' ');
}
}
else if (!isVaapiDecoder && isVaapiEncoder)
{
arg.Append("-vaapi_device ")
- .Append(encodingOptions.VaapiDevice)
+ .Append(options.VaapiDevice)
.Append(' ');
}
@@ -546,7 +591,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ && string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@@ -582,9 +627,8 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isVaapiDecoder && isVppTonemappingSupported)
{
arg.Append("-init_hw_device vaapi=va:")
- .Append(encodingOptions.VaapiDevice)
- .Append(' ')
- .Append("-init_hw_device qsv@va ")
+ .Append(options.VaapiDevice)
+ .Append(" -init_hw_device qsv@va ")
.Append("-hwaccel_output_format vaapi ");
}
@@ -593,7 +637,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
&& isNvdecDecoder)
{
// Fix for 'No decoder surfaces left' error. https://trac.ffmpeg.org/ticket/7562
@@ -601,22 +645,31 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (state.IsVideoRequest
- && ((string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
- && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder))
- || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
- && (isD3d11vaDecoder || isSwDecoder))))
+ && string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)
+ && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder))
{
- if (isTonemappingSupported)
+ if (!isCudaTonemappingSupported && isOpenclTonemappingSupported)
{
arg.Append("-init_hw_device opencl=ocl:")
- .Append(encodingOptions.OpenclDevice)
- .Append(' ')
- .Append("-filter_hw_device ocl ");
+ .Append(options.OpenclDevice)
+ .Append(" -filter_hw_device ocl ");
}
}
if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
+ && string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase)
+ && (isD3d11vaDecoder || isSwDecoder))
+ {
+ if (isOpenclTonemappingSupported)
+ {
+ arg.Append("-init_hw_device opencl=ocl:")
+ .Append(options.OpenclDevice)
+ .Append(" -filter_hw_device ocl ");
+ }
+ }
+
+ if (state.IsVideoRequest
+ && string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase))
{
arg.Append("-hwaccel videotoolbox ");
}
@@ -643,6 +696,11 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(" -i \"").Append(subtitlePath).Append('\"');
}
+ if (state.AudioStream != null && state.AudioStream.IsExternal)
+ {
+ arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
+ }
+
return arg.ToString();
}
@@ -726,54 +784,42 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
- var bitrate = state.OutputVideoBitrate;
-
- if (bitrate.HasValue)
+ if (state.OutputVideoBitrate == null)
{
- if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
- {
- // When crf is used with vpx, b:v becomes a max rate
- // https://trac.ffmpeg.org/wiki/Encode/VP9
- return string.Format(
- CultureInfo.InvariantCulture,
- " -maxrate:v {0} -bufsize:v {1} -b:v {0}",
- bitrate.Value,
- bitrate.Value * 2);
- }
+ return string.Empty;
+ }
- if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- " -b:v {0}",
- bitrate.Value);
- }
+ int bitrate = state.OutputVideoBitrate.Value;
- if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
- {
- // h264
- return string.Format(
- CultureInfo.InvariantCulture,
- " -maxrate {0} -bufsize {1}",
- bitrate.Value,
- bitrate.Value * 2);
- }
+ // Currently use the same buffer size for all encoders
+ int bufsize = bitrate * 2;
- // h264
- return string.Format(
- CultureInfo.InvariantCulture,
- " -b:v {0} -maxrate {0} -bufsize {1}",
- bitrate.Value,
- bitrate.Value * 2);
+ if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase))
+ {
+ // When crf is used with vpx, b:v becomes a max rate
+ // https://trac.ffmpeg.org/wiki/Encode/VP8
+ // https://trac.ffmpeg.org/wiki/Encode/VP9
+ return FormattableString.Invariant($" -maxrate:v {bitrate} -bufsize:v {bufsize} -b:v {bitrate}");
}
- return string.Empty;
+ if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return FormattableString.Invariant($" -b:v {bitrate}");
+ }
+
+ if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
+ {
+ return FormattableString.Invariant($" -maxrate {bitrate} -bufsize {bufsize}");
+ }
+
+ return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
- if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
+ if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double requestLevel))
{
if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
@@ -868,7 +914,7 @@ namespace MediaBrowser.Controller.MediaEncoding
CultureInfo.InvariantCulture,
"subtitles='{0}:si={1}'{2}",
_mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
- state.InternalSubtitleStreamOffset.ToString(_usCulture),
+ state.InternalSubtitleStreamOffset.ToString(CultureInfo.InvariantCulture),
// fallbackFontParam,
setPtsParam);
}
@@ -966,6 +1012,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the video bitrate to specify on the command line.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="videoEncoder">Video encoder to use.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <param name="defaultPreset">Default present to use for encoding.</param>
+ /// <returns>Video bitrate.</returns>
public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset)
{
var param = string.Empty;
@@ -1146,7 +1197,7 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -header_insertion_mode gop -gops_per_idr 1";
}
}
- else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
+ else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
{
// Values 0-3, 0 being highest quality but slower
var profileScore = 0;
@@ -1169,11 +1220,60 @@ namespace MediaBrowser.Controller.MediaEncoding
param += string.Format(
CultureInfo.InvariantCulture,
" -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
- profileScore.ToString(_usCulture),
+ profileScore.ToString(CultureInfo.InvariantCulture),
crf,
qmin,
qmax);
}
+ else if (string.Equals(videoEncoder, "libvpx-vp9", StringComparison.OrdinalIgnoreCase)) // vp9
+ {
+ // When `-deadline` is set to `good` or `best`, `-cpu-used` ranges from 0-5.
+ // When `-deadline` is set to `realtime`, `-cpu-used` ranges from 0-15.
+ // Resources:
+ // * https://trac.ffmpeg.org/wiki/Encode/VP9
+ // * https://superuser.com/questions/1586934
+ // * https://developers.google.com/media/vp9
+ param += encodingOptions.EncoderPreset switch
+ {
+ "veryslow" => " -deadline best -cpu-used 0",
+ "slower" => " -deadline best -cpu-used 2",
+ "slow" => " -deadline best -cpu-used 3",
+ "medium" => " -deadline good -cpu-used 0",
+ "fast" => " -deadline good -cpu-used 1",
+ "faster" => " -deadline good -cpu-used 2",
+ "veryfast" => " -deadline good -cpu-used 3",
+ "superfast" => " -deadline good -cpu-used 4",
+ "ultrafast" => " -deadline good -cpu-used 5",
+ _ => " -deadline good -cpu-used 1"
+ };
+
+ // TODO: until VP9 gets its own CRF setting, base CRF on H.265.
+ int h265Crf = encodingOptions.H265Crf;
+ int defaultVp9Crf = 31;
+ if (h265Crf >= 0 && h265Crf <= 51)
+ {
+ // This conversion factor is chosen to match the default CRF for H.265 to the
+ // recommended 1080p CRF from Google. The factor also maps the logarithmic CRF
+ // scale of x265 [0, 51] to that of VP9 [0, 63] relatively well.
+
+ // Resources:
+ // * https://developers.google.com/media/vp9/settings/vod
+ const float H265ToVp9CrfConversionFactor = 1.12F;
+
+ var vp9Crf = Convert.ToInt32(h265Crf * H265ToVp9CrfConversionFactor);
+
+ // Encoder allows for CRF values in the range [0, 63].
+ vp9Crf = Math.Clamp(vp9Crf, 0, 63);
+
+ param += FormattableString.Invariant($" -crf {vp9Crf}");
+ }
+ else
+ {
+ param += FormattableString.Invariant($" -crf {defaultVp9Crf}");
+ }
+
+ param += " -row-mt 1 -profile 1";
+ }
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
{
param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
@@ -1192,7 +1292,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var framerate = GetFramerateParam(state);
if (framerate.HasValue)
{
- param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(_usCulture));
+ param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(CultureInfo.InvariantCulture));
}
var targetVideoCodec = state.ActualOutputVideoCodec;
@@ -1296,7 +1396,7 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
// hevc_qsv use -level 51 instead of -level 153.
- if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
+ if (double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out double hevcLevel))
{
param += " -level " + (hevcLevel / 3);
}
@@ -1458,7 +1558,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a specific level was requested, the source must match or be less than
var level = state.GetRequestedLevel(videoStream.Codec);
if (!string.IsNullOrEmpty(level)
- && double.TryParse(level, NumberStyles.Any, _usCulture, out var requestLevel))
+ && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out var requestLevel))
{
if (!videoStream.Level.HasValue)
{
@@ -1692,7 +1792,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return 128000;
}
- public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
+ public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
{
var channels = state.OutputAudioChannels;
@@ -1706,7 +1806,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.AudioStream.Channels.Value > 5
&& !encodingOptions.DownMixAudioBoost.Equals(1))
{
- filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture));
+ filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
@@ -1745,7 +1845,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var request = state.BaseRequest;
- var inputChannels = audioStream?.Channels;
+ var inputChannels = audioStream.Channels;
if (inputChannels <= 0)
{
@@ -1904,10 +2004,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.AudioStream != null)
{
- args += string.Format(
- CultureInfo.InvariantCulture,
- " -map 0:{0}",
- state.AudioStream.Index);
+ if (state.AudioStream.IsExternal)
+ {
+ int externalAudioMapIndex = state.SubtitleStream != null && state.SubtitleStream.IsExternal ? 2 : 1;
+ int externalAudioStream = state.MediaSource.MediaStreams.Where(i => i.Path == state.AudioStream.Path).ToList().IndexOf(state.AudioStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map {0}:{1}",
+ externalAudioMapIndex,
+ externalAudioStream);
+ }
+ else
+ {
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0}",
+ state.AudioStream.Index);
+ }
}
else
{
@@ -1967,8 +2081,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
- /// Gets the graphical subtitle param.
+ /// Gets the graphical subtitle parameter.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>Graphical subtitle parameter.</returns>
public string GetGraphicalSubtitleParam(
EncodingJobInfo state,
EncodingOptions options,
@@ -1983,7 +2101,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isLinux = OperatingSystem.IsLinux();
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
@@ -1992,14 +2110,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var isQsvHevcEncoder = outputVideoCodec.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isNvdecDecoder = videoDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
- var isTonemappingSupported = IsTonemappingSupported(state, options);
- var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
+ var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+ var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+
+ var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
+ var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay;
+ var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
// Tonemapping and burn-in graphical subtitles requires overlay_vaapi.
// But it's still in ffmpeg mailing list. Disable it for now.
- if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported)
+ if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported)
{
return GetOutputSizeParam(state, options, outputVideoCodec);
}
@@ -2009,8 +2131,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Adjust the size of graphical subtitles to fit the video stream.
var videoStream = state.VideoStream;
- var inputWidth = videoStream?.Width;
- var inputHeight = videoStream?.Height;
+ var inputWidth = videoStream.Width;
+ var inputHeight = videoStream.Height;
var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
if (width.HasValue && height.HasValue)
@@ -2025,13 +2147,22 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(videoSizeParam)
&& !(isTonemappingSupportedOnQsv && isVppTonemappingSupported))
{
- // For QSV, feed it into hardware encoder now
+ // upload graphical subtitle to QSV
if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
{
videoSizeParam += ",hwupload=extra_hw_frames=64";
}
}
+
+ if (!string.IsNullOrEmpty(videoSizeParam))
+ {
+ // upload graphical subtitle to cuda
+ if (isNvdecDecoder && isNvencEncoder && isCudaOverlaySupported && isCudaFormatConversionSupported)
+ {
+ videoSizeParam += ",hwupload_cuda";
+ }
+ }
}
var mapPrefix = state.SubtitleStream.IsExternal ?
@@ -2044,9 +2175,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference)
// Always put the scaler before the overlay for better performance
- var retStr = !outputSizeParam.IsEmpty
- ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
- : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
+ var retStr = outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
@@ -2057,9 +2188,9 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- retStr = !outputSizeParam.IsEmpty
- ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""
- : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
+ retStr = outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\"";
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
@@ -2072,9 +2203,9 @@ namespace MediaBrowser.Controller.MediaEncoding
[sub]: SW scaling subtitle to FixedOutputSize
[base][sub]: SW overlay
*/
- retStr = !outputSizeParam.IsEmpty
- ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""
- : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
+ retStr = outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
}
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
@@ -2091,16 +2222,25 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (isLinux)
{
- retStr = !outputSizeParam.IsEmpty
- ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\""
- : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\"";
+ retStr = outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"";
}
}
else if (isNvdecDecoder && isNvencEncoder)
{
- retStr = !outputSizeParam.IsEmpty
- ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""
- : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"";
+ if (isCudaOverlaySupported && isCudaFormatConversionSupported)
+ {
+ retStr = outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]scale_cuda=format=yuv420p[base];[base][sub]overlay_cuda\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_cuda\"";
+ }
+ else
+ {
+ retStr = outputSizeParam.IsEmpty
+ ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay,format=nv12|yuv420p,hwupload_cuda\""
+ : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay,format=nv12|yuv420p,hwupload_cuda\"";
+ }
}
return string.Format(
@@ -2197,11 +2337,11 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiHevcEncoder = videoEncoder.Contains("hevc_vaapi", StringComparison.OrdinalIgnoreCase);
var isQsvH264Encoder = videoEncoder.Contains("h264_qsv", StringComparison.OrdinalIgnoreCase);
var isQsvHevcEncoder = videoEncoder.Contains("hevc_qsv", StringComparison.OrdinalIgnoreCase);
- var isTonemappingSupported = IsTonemappingSupported(state, options);
+ var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
- var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported))
+ var isP010PixFmtRequired = (isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported))
|| (isTonemappingSupportedOnQsv && isVppTonemappingSupported);
var outputPixFmt = "format=nv12";
@@ -2252,15 +2392,23 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputWidth = width.Value;
var outputHeight = height.Value;
- var isTonemappingSupported = IsTonemappingSupported(state, options);
+ var isNvencEncoder = videoEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
+ var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+ var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase);
- var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")");
+ var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
+ var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay;
+ var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var outputPixFmt = string.Empty;
if (isCudaFormatConversionSupported)
{
- outputPixFmt = "format=nv12";
- if (isTonemappingSupported && isTonemappingSupportedOnNvenc)
+ outputPixFmt = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder)
+ ? "format=yuv420p"
+ : "format=nv12";
+ if ((isOpenclTonemappingSupported || isCudaTonemappingSupported)
+ && isTonemappingSupportedOnNvenc)
{
outputPixFmt = "format=p010";
}
@@ -2303,8 +2451,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isExynosV4L2)
{
- var widthParam = requestedWidth.Value.ToString(_usCulture);
- var heightParam = requestedHeight.Value.ToString(_usCulture);
+ var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
+ var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture);
filters.Add(
string.Format(
@@ -2322,8 +2470,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
{
- var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture);
- var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture);
+ var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
+ var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2355,7 +2503,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else
{
- var widthParam = requestedWidth.Value.ToString(_usCulture);
+ var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
filters.Add(
string.Format(
@@ -2368,7 +2516,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a fixed height was requested
else if (requestedHeight.HasValue)
{
- var heightParam = requestedHeight.Value.ToString(_usCulture);
+ var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2391,7 +2539,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a max width was requested
else if (requestedMaxWidth.HasValue)
{
- var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture);
+ var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2414,7 +2562,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// If a max height was requested
else if (requestedMaxHeight.HasValue)
{
- var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture);
+ var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
if (isExynosV4L2)
{
@@ -2486,6 +2634,13 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam);
}
+ /// <summary>
+ /// Gets the output size parameter.
+ /// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>The output size parameter.</returns>
public string GetOutputSizeParam(
EncodingJobInfo state,
EncodingOptions options,
@@ -2496,8 +2651,13 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the output size parameter.
/// If we're going to put a fixed size on the command line, this will calculate it.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>The output size parameter.</returns>
public string GetOutputSizeParamInternal(
EncodingJobInfo state,
EncodingOptions options,
@@ -2526,16 +2686,21 @@ namespace MediaBrowser.Controller.MediaEncoding
var isNvencEncoder = outputVideoCodec.Contains("nvenc", StringComparison.OrdinalIgnoreCase);
var isCuvidH264Decoder = videoDecoder.Contains("h264_cuvid", StringComparison.OrdinalIgnoreCase);
var isCuvidHevcDecoder = videoDecoder.Contains("hevc_cuvid", StringComparison.OrdinalIgnoreCase);
+ var isCuvidVp9Decoder = videoDecoder.Contains("vp9_cuvid", StringComparison.OrdinalIgnoreCase);
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isLinux = OperatingSystem.IsLinux();
var isColorDepth10 = IsColorDepth10(state);
- var isTonemappingSupported = IsTonemappingSupported(state, options);
- var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
- var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder);
+
+ var isTonemappingSupportedOnNvenc = string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && (isNvdecDecoder || isCuvidHevcDecoder || isCuvidVp9Decoder || isSwDecoder);
var isTonemappingSupportedOnAmf = string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && (isD3d11vaDecoder || isSwDecoder);
var isTonemappingSupportedOnVaapi = string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isVaapiH264Encoder || isVaapiHevcEncoder);
var isTonemappingSupportedOnQsv = string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) && isVaapiDecoder && (isQsvH264Encoder || isQsvHevcEncoder);
+ var isOpenclTonemappingSupported = IsOpenclTonemappingSupported(state, options);
+ var isVppTonemappingSupported = IsVppTonemappingSupported(state, options);
+ var isCudaTonemappingSupported = IsCudaTonemappingSupported(state, options);
+ var mediaEncoderVersion = _mediaEncoder.GetMediaEncoderVersion();
+ var isCudaOverlaySupported = _mediaEncoder.SupportsFilter("overlay_cuda") && mediaEncoderVersion != null && mediaEncoderVersion >= _minVersionForCudaOverlay;
var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@@ -2547,19 +2712,25 @@ namespace MediaBrowser.Controller.MediaEncoding
var isScalingInAdvance = false;
var isCudaDeintInAdvance = false;
var isHwuploadCudaRequired = false;
+ var isNoTonemapFilterApplied = true;
var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
// Add OpenCL tonemapping filter for NVENC/AMF/VAAPI.
- if (isTonemappingSupportedOnNvenc || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported))
+ if ((isTonemappingSupportedOnNvenc && !isCudaTonemappingSupported) || isTonemappingSupportedOnAmf || (isTonemappingSupportedOnVaapi && !isVppTonemappingSupported))
{
- // Currently only with the use of NVENC decoder can we get a decent performance.
- // Currently only the HEVC/H265 format is supported with NVDEC decoder.
// NVIDIA Pascal and Turing or higher are recommended.
// AMD Polaris and Vega or higher are recommended.
// Intel Kaby Lake or newer is required.
- if (isTonemappingSupported)
+ if (isOpenclTonemappingSupported)
{
+ isNoTonemapFilterApplied = false;
+ var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer);
+ if (!string.IsNullOrEmpty(inputHdrParams))
+ {
+ filters.Add(inputHdrParams);
+ }
+
var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
if (options.TonemappingParam != 0)
@@ -2631,7 +2802,11 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwdownload,format=p010");
}
- if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder)
+ if (isNvdecDecoder
+ || isCuvidHevcDecoder
+ || isCuvidVp9Decoder
+ || isSwDecoder
+ || isD3d11vaDecoder)
{
// Upload the HDR10 or HLG data to the OpenCL device,
// use tonemap_opencl filter for tone mapping,
@@ -2639,6 +2814,14 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwupload");
}
+ // Fallback to hable if bt2390 is chosen but not supported in tonemap_opencl.
+ var isBt2390SupportedInOpenclTonemap = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TonemapOpenclBt2390);
+ if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase)
+ && !isBt2390SupportedInOpenclTonemap)
+ {
+ options.TonemappingAlgorithm = "hable";
+ }
+
filters.Add(
string.Format(
CultureInfo.InvariantCulture,
@@ -2650,7 +2833,11 @@ namespace MediaBrowser.Controller.MediaEncoding
options.TonemappingParam,
options.TonemappingRange));
- if (isNvdecDecoder || isCuvidHevcDecoder || isSwDecoder || isD3d11vaDecoder)
+ if (isNvdecDecoder
+ || isCuvidHevcDecoder
+ || isCuvidVp9Decoder
+ || isSwDecoder
+ || isD3d11vaDecoder)
{
filters.Add("hwdownload");
filters.Add("format=nv12");
@@ -2666,12 +2853,18 @@ namespace MediaBrowser.Controller.MediaEncoding
// Reverse the data route from opencl to vaapi.
filters.Add("hwmap=derive_device=vaapi:reverse=1");
}
+
+ var outputSdrParams = GetOutputSdrParams(options.TonemappingRange);
+ if (!string.IsNullOrEmpty(outputSdrParams))
+ {
+ filters.Add(outputSdrParams);
+ }
}
}
// When the input may or may not be hardware VAAPI decodable.
if ((isVaapiH264Encoder || isVaapiHevcEncoder)
- && !(isTonemappingSupportedOnVaapi && (isTonemappingSupported || isVppTonemappingSupported)))
+ && !(isTonemappingSupportedOnVaapi && (isOpenclTonemappingSupported || isVppTonemappingSupported)))
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
@@ -2779,6 +2972,61 @@ namespace MediaBrowser.Controller.MediaEncoding
request.MaxHeight));
}
+ // Add Cuda tonemapping filter.
+ if (isNvdecDecoder && isCudaTonemappingSupported)
+ {
+ isNoTonemapFilterApplied = false;
+ var inputHdrParams = GetInputHdrParams(videoStream.ColorTransfer);
+ if (!string.IsNullOrEmpty(inputHdrParams))
+ {
+ filters.Add(inputHdrParams);
+ }
+
+ var parameters = (hasGraphicalSubs && isCudaOverlaySupported && isNvencEncoder)
+ ? "tonemap_cuda=format=yuv420p:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}"
+ : "tonemap_cuda=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:peak={1}:desat={2}";
+
+ if (options.TonemappingParam != 0)
+ {
+ parameters += ":param={3}";
+ }
+
+ if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ {
+ parameters += ":range={4}";
+ }
+
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ parameters,
+ options.TonemappingAlgorithm,
+ options.TonemappingPeak,
+ options.TonemappingDesat,
+ options.TonemappingParam,
+ options.TonemappingRange));
+
+ if (isLibX264Encoder
+ || isLibX265Encoder
+ || hasTextSubs
+ || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder))
+ {
+ if (isNvencEncoder)
+ {
+ isHwuploadCudaRequired = true;
+ }
+
+ filters.Add("hwdownload");
+ filters.Add("format=nv12");
+ }
+
+ var outputSdrParams = GetOutputSdrParams(options.TonemappingRange);
+ if (!string.IsNullOrEmpty(outputSdrParams))
+ {
+ filters.Add(outputSdrParams);
+ }
+ }
+
// Add VPP tonemapping filter for VAAPI.
// Full hardware based video post processing, faster than OpenCL but lacks fine tuning options.
if ((isTonemappingSupportedOnVaapi || isTonemappingSupportedOnQsv)
@@ -2788,10 +3036,10 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Another case is when using Nvenc decoder.
- if (isNvdecDecoder && !isTonemappingSupported)
+ if (isNvdecDecoder && !isOpenclTonemappingSupported && !isCudaTonemappingSupported)
{
var codec = videoStream.Codec;
- var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilter("scale_cuda", "Output format (default \"same\")");
+ var isCudaFormatConversionSupported = _mediaEncoder.SupportsFilterWithOption(FilterOptionType.ScaleCudaFormat);
// Assert 10-bit hardware decodable
if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
@@ -2800,7 +3048,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isCudaFormatConversionSupported)
{
- if (isLibX264Encoder || isLibX265Encoder || hasSubs)
+ if (isLibX264Encoder
+ || isLibX265Encoder
+ || hasTextSubs
+ || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder))
{
if (isNvencEncoder)
{
@@ -2827,7 +3078,11 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Assert 8-bit hardware decodable
- else if (!isColorDepth10 && (isLibX264Encoder || isLibX265Encoder || hasSubs))
+ else if (!isColorDepth10
+ && (isLibX264Encoder
+ || isLibX265Encoder
+ || hasTextSubs
+ || (hasGraphicalSubs && !isCudaOverlaySupported && isNvencEncoder)))
{
if (isNvencEncoder)
{
@@ -2848,7 +3103,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// Convert hw context from ocl to va.
// For tonemapping and text subs burn-in.
- if (isTonemappingSupportedOnVaapi && isTonemappingSupported && !isVppTonemappingSupported)
+ if (isTonemappingSupportedOnVaapi && isOpenclTonemappingSupported && !isVppTonemappingSupported)
{
filters.Add("scale_vaapi");
}
@@ -2894,6 +3149,17 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwupload_cuda");
}
+ // If no tonemap filter is applied,
+ // tag the video range as SDR to prevent the encoder from encoding HDR video.
+ if (isNoTonemapFilterApplied)
+ {
+ var outputSdrParams = GetOutputSdrParams(null);
+ if (!string.IsNullOrEmpty(outputSdrParams))
+ {
+ filters.Add(outputSdrParams);
+ }
+ }
+
var output = string.Empty;
if (filters.Count > 0)
{
@@ -2906,26 +3172,56 @@ namespace MediaBrowser.Controller.MediaEncoding
return output;
}
+ public static string GetInputHdrParams(string colorTransfer)
+ {
+ if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
+ {
+ // HLG
+ return "setparams=color_primaries=bt2020:color_trc=arib-std-b67:colorspace=bt2020nc";
+ }
+ else
+ {
+ // HDR10
+ return "setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc";
+ }
+ }
+
+ public static string GetOutputSdrParams(string tonemappingRange)
+ {
+ // SDR
+ if (string.Equals(tonemappingRange, "tv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv";
+ }
+
+ if (string.Equals(tonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
+ {
+ return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=pc";
+ }
+
+ return "setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709";
+ }
+
/// <summary>
/// Gets the number of threads.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="encodingOptions">Encoding options.</param>
+ /// <param name="outputVideoCodec">Video codec to use.</param>
+ /// <returns>Number of threads.</returns>
#nullable enable
public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{
- if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
- {
- // per docs:
- // -threads number of threads to use for encoding, can't be 0 [auto] with VP8
- // (recommended value : number of real cores - 1)
- return Math.Max(Environment.ProcessorCount - 1, 1);
- }
+ // VP8 and VP9 encoders must have their thread counts set.
+ bool mustSetThreadCount = string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(outputVideoCodec, "libvpx-vp9", StringComparison.OrdinalIgnoreCase);
var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
- // Automatic
if (threads <= 0)
{
- return 0;
+ // Automatically set thread count
+ return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0;
}
else if (threads >= Environment.ProcessorCount)
{
@@ -3067,7 +3363,7 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier += " " + videoDecoder;
if (!IsCopyCodec(state.OutputVideoCodec)
- && (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
+ && videoDecoder.Contains("cuvid", StringComparison.OrdinalIgnoreCase))
{
var videoStream = state.VideoStream;
var inputWidth = videoStream?.Width;
@@ -3076,7 +3372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
- if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
+ if (videoDecoder.Contains("cuvid", StringComparison.OrdinalIgnoreCase)
&& width.HasValue
&& height.HasValue)
{
@@ -3372,8 +3668,13 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)
&& IsVppTonemappingSupported(state, encodingOptions))
{
- // Since tonemap_vaapi only support HEVC for now, no need to check the codec again.
- return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
+ var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+ var isQsvEncoder = outputVideoCodec.Contains("qsv", StringComparison.OrdinalIgnoreCase);
+ if (isQsvEncoder)
+ {
+ // Since tonemap_vaapi only support HEVC for now, no need to check the codec again.
+ return GetHwaccelType(state, encodingOptions, "hevc", isColorDepth10);
+ }
}
if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
@@ -3552,6 +3853,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets a hw decoder name.
/// </summary>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="decoder">Decoder to use.</param>
+ /// <param name="videoCodec">Video codec to use.</param>
+ /// <param name="isColorDepth10">Specifies if color depth 10.</param>
+ /// <returns>Hardware decoder name.</returns>
public string GetHwDecoderName(EncodingOptions options, string decoder, string videoCodec, bool isColorDepth10)
{
var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoder) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
@@ -3570,10 +3876,15 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets a hwaccel type to use as a hardware decoder(dxva/vaapi) depending on the system.
/// </summary>
+ /// <param name="state">Encoding state.</param>
+ /// <param name="options">Encoding options.</param>
+ /// <param name="videoCodec">Video codec to use.</param>
+ /// <param name="isColorDepth10">Specifies if color depth 10.</param>
+ /// <returns>Hardware accelerator type.</returns>
public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec, bool isColorDepth10)
{
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+ var isWindows = OperatingSystem.IsWindows();
+ var isLinux = OperatingSystem.IsLinux();
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
@@ -3828,15 +4139,15 @@ namespace MediaBrowser.Controller.MediaEncoding
if (bitrate.HasValue)
{
- args += " -ab " + bitrate.Value.ToString(_usCulture);
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += GetAudioFilterParam(state, encodingOptions, false);
+ args += GetAudioFilterParam(state, encodingOptions);
return args;
}
@@ -3849,12 +4160,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (bitrate.HasValue)
{
- audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture));
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
}
if (state.OutputAudioChannels.HasValue)
{
- audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(_usCulture));
+ audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
// opus will fail on 44100
@@ -3862,7 +4173,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (state.OutputAudioSampleRate.HasValue)
{
- audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture));
+ audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
}
@@ -3896,6 +4207,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (videoStream != null)
{
+ if (videoStream.BitDepth.HasValue)
+ {
+ return videoStream.BitDepth.Value == 10;
+ }
+
if (!string.IsNullOrEmpty(videoStream.PixelFormat))
{
result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase);
@@ -3915,12 +4231,6 @@ namespace MediaBrowser.Controller.MediaEncoding
return true;
}
}
-
- result = (videoStream.BitDepth ?? 8) == 10;
- if (result)
- {
- return true;
- }
}
return result;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index bc0318ad7..e92c4a08a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, SA1401
using System;
using System.Collections.Generic;
@@ -20,6 +20,44 @@ namespace MediaBrowser.Controller.MediaEncoding
// For now, a common base class until the API and MediaEncoding classes are unified
public class EncodingJobInfo
{
+ public int? OutputAudioBitrate;
+ public int? OutputAudioChannels;
+
+ private TranscodeReason[] _transcodeReasons = null;
+
+ public EncodingJobInfo(TranscodingJobType jobType)
+ {
+ TranscodingType = jobType;
+ RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SupportedAudioCodecs = Array.Empty<string>();
+ SupportedVideoCodecs = Array.Empty<string>();
+ SupportedSubtitleCodecs = Array.Empty<string>();
+ }
+
+ public TranscodeReason[] TranscodeReasons
+ {
+ get
+ {
+ if (_transcodeReasons == null)
+ {
+ if (BaseRequest.TranscodeReasons == null)
+ {
+ return Array.Empty<TranscodeReason>();
+ }
+
+ _transcodeReasons = BaseRequest.TranscodeReasons
+ .Split(',')
+ .Where(i => !string.IsNullOrEmpty(i))
+ .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
+ .ToArray();
+ }
+
+ return _transcodeReasons;
+ }
+ }
+
+ public IProgress<double> Progress { get; set; }
+
public MediaStream VideoStream { get; set; }
public VideoType VideoType { get; set; }
@@ -58,40 +96,6 @@ namespace MediaBrowser.Controller.MediaEncoding
public string MimeType { get; set; }
- public string GetMimeType(string outputPath, bool enableStreamDefault = true)
- {
- if (!string.IsNullOrEmpty(MimeType))
- {
- return MimeType;
- }
-
- return MimeTypes.GetMimeType(outputPath, enableStreamDefault);
- }
-
- private TranscodeReason[] _transcodeReasons = null;
-
- public TranscodeReason[] TranscodeReasons
- {
- get
- {
- if (_transcodeReasons == null)
- {
- if (BaseRequest.TranscodeReasons == null)
- {
- return Array.Empty<TranscodeReason>();
- }
-
- _transcodeReasons = BaseRequest.TranscodeReasons
- .Split(',')
- .Where(i => !string.IsNullOrEmpty(i))
- .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
- .ToArray();
- }
-
- return _transcodeReasons;
- }
- }
-
public bool IgnoreInputDts => MediaSource.IgnoreDts;
public bool IgnoreInputIndex => MediaSource.IgnoreIndex;
@@ -144,196 +148,17 @@ namespace MediaBrowser.Controller.MediaEncoding
public BaseEncodingJobOptions BaseRequest { get; set; }
- public long? StartTimeTicks => BaseRequest.StartTimeTicks;
-
- public bool CopyTimestamps => BaseRequest.CopyTimestamps;
-
- public int? OutputAudioBitrate;
- public int? OutputAudioChannels;
-
- public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
- {
- var videoStream = VideoStream;
- var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
-
- if (!isInputInterlaced)
- {
- return false;
- }
-
- // Support general param
- if (BaseRequest.DeInterlace)
- {
- return true;
- }
-
- if (!string.IsNullOrEmpty(videoCodec))
- {
- if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return forceDeinterlaceIfSourceIsInterlaced && isInputInterlaced;
- }
-
- public string[] GetRequestedProfiles(string codec)
- {
- if (!string.IsNullOrEmpty(BaseRequest.Profile))
- {
- return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var profile = BaseRequest.GetOption(codec, "profile");
-
- if (!string.IsNullOrEmpty(profile))
- {
- return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- return Array.Empty<string>();
- }
-
- public string GetRequestedLevel(string codec)
- {
- if (!string.IsNullOrEmpty(BaseRequest.Level))
- {
- return BaseRequest.Level;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- return BaseRequest.GetOption(codec, "level");
- }
-
- return null;
- }
-
- public int? GetRequestedMaxRefFrames(string codec)
- {
- if (BaseRequest.MaxRefFrames.HasValue)
- {
- return BaseRequest.MaxRefFrames;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "maxrefframes");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- return null;
- }
-
- public int? GetRequestedVideoBitDepth(string codec)
- {
- if (BaseRequest.MaxVideoBitDepth.HasValue)
- {
- return BaseRequest.MaxVideoBitDepth;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "videobitdepth");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- return null;
- }
-
- public int? GetRequestedAudioBitDepth(string codec)
- {
- if (BaseRequest.MaxAudioBitDepth.HasValue)
- {
- return BaseRequest.MaxAudioBitDepth;
- }
-
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "audiobitdepth");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- return null;
- }
-
- public int? GetRequestedAudioChannels(string codec)
- {
- if (!string.IsNullOrEmpty(codec))
- {
- var value = BaseRequest.GetOption(codec, "audiochannels");
- if (!string.IsNullOrEmpty(value)
- && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
- {
- return result;
- }
- }
-
- if (BaseRequest.MaxAudioChannels.HasValue)
- {
- return BaseRequest.MaxAudioChannels;
- }
-
- if (BaseRequest.AudioChannels.HasValue)
- {
- return BaseRequest.AudioChannels;
- }
-
- if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
- {
- return BaseRequest.TranscodingMaxAudioChannels;
- }
-
- return null;
- }
-
public bool IsVideoRequest { get; set; }
public TranscodingJobType TranscodingType { get; set; }
- public EncodingJobInfo(TranscodingJobType jobType)
- {
- TranscodingType = jobType;
- RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- SupportedAudioCodecs = Array.Empty<string>();
- SupportedVideoCodecs = Array.Empty<string>();
- SupportedSubtitleCodecs = Array.Empty<string>();
- }
+ public long? StartTimeTicks => BaseRequest.StartTimeTicks;
+
+ public bool CopyTimestamps => BaseRequest.CopyTimestamps;
public bool IsSegmentedLiveStream
=> TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue;
- public bool EnableBreakOnNonKeyFrames(string videoCodec)
- {
- if (TranscodingType != TranscodingJobType.Progressive)
- {
- if (IsSegmentedLiveStream)
- {
- return false;
- }
-
- return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
- }
-
- return false;
- }
-
public int? TotalOutputBitrate => (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0);
public int? OutputWidth
@@ -597,7 +422,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
- return VideoStream?.Codec;
+ return VideoStream.Codec;
}
return OutputVideoCodec;
@@ -615,7 +440,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
{
- return AudioStream?.Codec;
+ return AudioStream.Codec;
}
return OutputAudioCodec;
@@ -682,6 +507,21 @@ namespace MediaBrowser.Controller.MediaEncoding
public int HlsListSize => 0;
+ public bool EnableBreakOnNonKeyFrames(string videoCodec)
+ {
+ if (TranscodingType != TranscodingJobType.Progressive)
+ {
+ if (IsSegmentedLiveStream)
+ {
+ return false;
+ }
+
+ return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
+ }
+
+ return false;
+ }
+
private int? GetMediaStreamCount(MediaStreamType type, int limit)
{
var count = MediaSource.GetStreamCount(type);
@@ -694,7 +534,172 @@ namespace MediaBrowser.Controller.MediaEncoding
return count;
}
- public IProgress<double> Progress { get; set; }
+ public string GetMimeType(string outputPath, bool enableStreamDefault = true)
+ {
+ if (!string.IsNullOrEmpty(MimeType))
+ {
+ return MimeType;
+ }
+
+ if (enableStreamDefault)
+ {
+ return MimeTypes.GetMimeType(outputPath);
+ }
+
+ return MimeTypes.GetMimeType(outputPath, null);
+ }
+
+ public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
+ {
+ var videoStream = VideoStream;
+ var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
+
+ if (!isInputInterlaced)
+ {
+ return false;
+ }
+
+ // Support general param
+ if (BaseRequest.DeInterlace)
+ {
+ return true;
+ }
+
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return forceDeinterlaceIfSourceIsInterlaced;
+ }
+
+ public string[] GetRequestedProfiles(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.Profile))
+ {
+ return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var profile = BaseRequest.GetOption(codec, "profile");
+
+ if (!string.IsNullOrEmpty(profile))
+ {
+ return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+
+ return Array.Empty<string>();
+ }
+
+ public string GetRequestedLevel(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.Level))
+ {
+ return BaseRequest.Level;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ return BaseRequest.GetOption(codec, "level");
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedMaxRefFrames(string codec)
+ {
+ if (BaseRequest.MaxRefFrames.HasValue)
+ {
+ return BaseRequest.MaxRefFrames;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "maxrefframes");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedVideoBitDepth(string codec)
+ {
+ if (BaseRequest.MaxVideoBitDepth.HasValue)
+ {
+ return BaseRequest.MaxVideoBitDepth;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "videobitdepth");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedAudioBitDepth(string codec)
+ {
+ if (BaseRequest.MaxAudioBitDepth.HasValue)
+ {
+ return BaseRequest.MaxAudioBitDepth;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiobitdepth");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedAudioChannels(string codec)
+ {
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiochannels");
+ if (!string.IsNullOrEmpty(value)
+ && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
+ {
+ return result;
+ }
+ }
+
+ if (BaseRequest.MaxAudioChannels.HasValue)
+ {
+ return BaseRequest.MaxAudioChannels;
+ }
+
+ if (BaseRequest.AudioChannels.HasValue)
+ {
+ return BaseRequest.AudioChannels;
+ }
+
+ if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
+ {
+ return BaseRequest.TranscodingMaxAudioChannels;
+ }
+
+ return null;
+ }
public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
{
diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
new file mode 100644
index 000000000..7ce707b19
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ /// <summary>
+ /// Enum FilterOptionType.
+ /// </summary>
+ public enum FilterOptionType
+ {
+ /// <summary>
+ /// The scale_cuda_format.
+ /// </summary>
+ ScaleCudaFormat = 0,
+
+ /// <summary>
+ /// The tonemap_cuda_name.
+ /// </summary>
+ TonemapCudaName = 1,
+
+ /// <summary>
+ /// The tonemap_opencl_bt2390.
+ /// </summary>
+ TonemapOpenclBt2390 = 2
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
index 773547872..8ce40a58d 100644
--- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
@@ -16,6 +16,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Refreshes the chapter images.
/// </summary>
+ /// <param name="video">Video to use.</param>
+ /// <param name="directoryService">Directory service to use.</param>
+ /// <param name="chapters">Set of chapters to refresh.</param>
+ /// <param name="extractImages">Option to extract images.</param>
+ /// <param name="saveChapters">Option to save chapters.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 76a9fd7c7..1418e583e 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -7,10 +7,10 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.System;
namespace MediaBrowser.Controller.MediaEncoding
{
@@ -20,11 +20,6 @@ namespace MediaBrowser.Controller.MediaEncoding
public interface IMediaEncoder : ITranscoderSupport
{
/// <summary>
- /// Gets location of the discovered FFmpeg tool.
- /// </summary>
- FFmpegLocation EncoderLocation { get; }
-
- /// <summary>
/// Gets the encoder path.
/// </summary>
/// <value>The encoder path.</value>
@@ -55,9 +50,21 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Whether given filter is supported.
/// </summary>
/// <param name="filter">The filter.</param>
+ /// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
+ bool SupportsFilter(string filter);
+
+ /// <summary>
+ /// Whether filter is supported with the given option.
+ /// </summary>
/// <param name="option">The option.</param>
/// <returns><c>true</c> if the filter is supported, <c>false</c> otherwise.</returns>
- bool SupportsFilter(string filter, string option);
+ bool SupportsFilterWithOption(FilterOptionType option);
+
+ /// <summary>
+ /// Get the version of media encoder.
+ /// </summary>
+ /// <returns>The version of media encoder.</returns>
+ Version GetMediaEncoderVersion();
/// <summary>
/// Extracts the audio image.
@@ -71,24 +78,28 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Extracts the video image.
/// </summary>
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="videoStream">Media stream information.</param>
+ /// <param name="threedFormat">Video 3D format.</param>
+ /// <param name="offset">Time offset.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Location of video image.</returns>
Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
- Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
-
/// <summary>
- /// Extracts the video images on interval.
+ /// Extracts the video image.
/// </summary>
- Task ExtractVideoImagesOnInterval(
- string inputFile,
- string container,
- MediaStream videoStream,
- MediaSourceInfo mediaSource,
- Video3DFormat? threedFormat,
- TimeSpan interval,
- string targetDirectory,
- string filenamePrefix,
- int? maxWidth,
- CancellationToken cancellationToken);
+ /// <param name="inputFile">Input file.</param>
+ /// <param name="container">Video container type.</param>
+ /// <param name="mediaSource">Media source information.</param>
+ /// <param name="imageStream">Media stream information.</param>
+ /// <param name="imageStreamIndex">Index of the stream to extract from.</param>
+ /// <param name="targetFormat">The format of the file to write.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <returns>Location of video image.</returns>
+ Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken);
/// <summary>
/// Gets the media info.
@@ -122,10 +133,24 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.String.</returns>
string EscapeSubtitleFilterPath(string path);
+ /// <summary>
+ /// Sets the path to find FFmpeg.
+ /// </summary>
void SetFFmpegPath();
+ /// <summary>
+ /// Updates the encoder path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="pathType">The type of path.</param>
void UpdateEncoderPath(string path, string pathType);
+ /// <summary>
+ /// Gets the primary playlist of .vob files.
+ /// </summary>
+ /// <param name="path">The to the .vob files.</param>
+ /// <param name="titleNumber">The title number to start with.</param>
+ /// <returns>A playlist.</returns>
IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
index 3fb2c47e1..4483cf708 100644
--- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -15,6 +15,14 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the subtitles.
/// </summary>
+ /// <param name="item">Item to use.</param>
+ /// <param name="mediaSourceId">Media source.</param>
+ /// <param name="subtitleStreamIndex">Subtitle stream to use.</param>
+ /// <param name="outputFormat">Output format to use.</param>
+ /// <param name="startTimeTicks">Start time.</param>
+ /// <param name="endTimeTicks">End time.</param>
+ /// <param name="preserveOriginalTimestamps">Option to preserve original timestamps.</param>
+ /// <param name="cancellationToken">The cancellation token for the operation.</param>
/// <returns>Task{Stream}.</returns>
Task<Stream> GetSubtitles(
BaseItem item,
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index b23c95112..933f440ac 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -6,7 +6,6 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -14,7 +13,6 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public class JobLogger
{
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
private readonly ILogger _logger;
public JobLogger(ILogger logger)
@@ -88,7 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var rate = parts[i + 1];
- if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
+ if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
framerate = val;
}
@@ -97,7 +95,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var rate = part.Split('=', 2)[^1];
- if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
+ if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
framerate = val;
}
@@ -107,7 +105,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var time = part.Split('=', 2)[^1];
- if (TimeSpan.TryParse(time, _usCulture, out var val))
+ if (TimeSpan.TryParse(time, CultureInfo.InvariantCulture, out var val))
{
var currentMs = startMs + val.TotalMilliseconds;
@@ -121,7 +119,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var size = part.Split('=', 2)[^1];
int? scale = null;
- if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
+ if (size.Contains("kb", StringComparison.OrdinalIgnoreCase))
{
scale = 1024;
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -129,7 +127,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (scale.HasValue)
{
- if (long.TryParse(size, NumberStyles.Any, _usCulture, out var val))
+ if (long.TryParse(size, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
bytesTranscoded = val * scale.Value;
}
@@ -140,7 +138,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var rate = part.Split('=', 2)[^1];
int? scale = null;
- if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
+ if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase))
{
scale = 1024;
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
@@ -148,7 +146,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (scale.HasValue)
{
- if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val))
+ if (float.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out var val))
{
bitRate = (int)Math.Ceiling(val * scale.Value);
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index d8995ce74..0813a8e7d 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, SA1306, SA1401
using System;
using System.Collections.Generic;
@@ -31,6 +31,21 @@ namespace MediaBrowser.Controller.Net
new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
/// <summary>
+ /// The logger.
+ /// </summary>
+ protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
+
+ protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
+ {
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ Logger = logger;
+ }
+
+ /// <summary>
/// Gets the type used for the messages sent to the client.
/// </summary>
/// <value>The type.</value>
@@ -55,21 +70,6 @@ namespace MediaBrowser.Controller.Net
protected abstract Task<TReturnDataType> GetDataToSend();
/// <summary>
- /// The logger.
- /// </summary>
- protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
-
- protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
- {
- if (logger == null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
-
- Logger = logger;
- }
-
- /// <summary>
/// Processes the message.
/// </summary>
/// <param name="message">The message.</param>
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index d15c6d318..a7da740e0 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -1,3 +1,4 @@
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -12,6 +13,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Authorization information. Null if unauthenticated.</returns>
- AuthorizationInfo Authenticate(HttpRequest request);
+ Task<AuthorizationInfo> Authenticate(HttpRequest request);
}
}
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 0d310548d..5c6ca43d1 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -1,3 +1,4 @@
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@@ -11,14 +12,14 @@ namespace MediaBrowser.Controller.Net
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
- /// <returns>AuthorizationInfo.</returns>
- AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
+ /// <returns>A task containing the authorization info.</returns>
+ Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext);
/// <summary>
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
- /// <returns>AuthorizationInfo.</returns>
- AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
+ /// <returns>A <see cref="Task"/> containing the authorization info.</returns>
+ Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext);
}
}
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
index 6b896b41f..b48181b3f 100644
--- a/MediaBrowser.Controller/Net/ISessionContext.cs
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Session;
using Microsoft.AspNetCore.Http;
@@ -8,12 +9,12 @@ namespace MediaBrowser.Controller.Net
{
public interface ISessionContext
{
- SessionInfo GetSession(object requestContext);
+ Task<SessionInfo> GetSession(object requestContext);
- User? GetUser(object requestContext);
+ Task<User?> GetUser(object requestContext);
- SessionInfo GetSession(HttpContext requestContext);
+ Task<SessionInfo> GetSession(HttpContext requestContext);
- User? GetUser(HttpContext requestContext);
+ Task<User?> GetUser(HttpContext requestContext);
}
}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 0a9073e7f..a084f9196 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -49,17 +49,17 @@ namespace MediaBrowser.Controller.Persistence
/// <summary>
/// Gets chapters for an item.
/// </summary>
- /// <param name="id">The item.</param>
+ /// <param name="item">The item.</param>
/// <returns>The list of chapter info.</returns>
- List<ChapterInfo> GetChapters(BaseItem id);
+ List<ChapterInfo> GetChapters(BaseItem item);
/// <summary>
/// Gets a single chapter for an item.
/// </summary>
- /// <param name="id">The item.</param>
+ /// <param name="item">The item.</param>
/// <param name="index">The chapter index.</param>
/// <returns>The chapter info at the specified index.</returns>
- ChapterInfo GetChapter(BaseItem id, int index);
+ ChapterInfo GetChapter(BaseItem item, int index);
/// <summary>
/// Saves the chapters.
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
index 5fa5834c8..c43acfb6d 100644
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -18,7 +18,6 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken);
/// <summary>
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 3eaf23515..89f3bdf46 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -31,24 +31,18 @@ namespace MediaBrowser.Controller.Playlists
".zpl"
};
- public Guid OwnerUserId { get; set; }
-
- public Share[] Shares { get; set; }
-
public Playlist()
{
Shares = Array.Empty<Share>();
}
+ public Guid OwnerUserId { get; set; }
+
+ public Share[] Shares { get; set; }
+
[JsonIgnore]
public bool IsFile => IsPlaylistFile(Path);
- public static bool IsPlaylistFile(string path)
- {
- // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
- return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
- }
-
[JsonIgnore]
public override string ContainingFolderPath
{
@@ -80,6 +74,41 @@ namespace MediaBrowser.Controller.Playlists
[JsonIgnore]
public override bool SupportsCumulativeRunTimeTicks => true;
+ [JsonIgnore]
+ public override bool IsPreSorted => true;
+
+ public string PlaylistMediaType { get; set; }
+
+ [JsonIgnore]
+ public override string MediaType => PlaylistMediaType;
+
+ [JsonIgnore]
+ private bool IsSharedItem
+ {
+ get
+ {
+ var path = Path;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+
+ return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
+ }
+ }
+
+ public static bool IsPlaylistFile(string path)
+ {
+ // The path will sometimes be a directory and "Path.HasExtension" returns true if the name contains a '.' (dot).
+ return System.IO.Path.HasExtension(path) && !Directory.Exists(path);
+ }
+
+ public void SetMediaType(string value)
+ {
+ PlaylistMediaType = value;
+ }
+
public override double GetDefaultPrimaryImageAspectRatio()
{
return 1;
@@ -160,7 +189,7 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { nameof(Audio) },
+ IncludeItemTypes = new[] { BaseItemKind.Audio },
GenreIds = new[] { musicGenre.Id },
OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
@@ -172,7 +201,7 @@ namespace MediaBrowser.Controller.Playlists
return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
Recursive = true,
- IncludeItemTypes = new[] { nameof(Audio) },
+ IncludeItemTypes = new[] { BaseItemKind.Audio },
ArtistIds = new[] { musicArtist.Id },
OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
@@ -197,35 +226,6 @@ namespace MediaBrowser.Controller.Playlists
return new[] { item };
}
- [JsonIgnore]
- public override bool IsPreSorted => true;
-
- public string PlaylistMediaType { get; set; }
-
- [JsonIgnore]
- public override string MediaType => PlaylistMediaType;
-
- public void SetMediaType(string value)
- {
- PlaylistMediaType = value;
- }
-
- [JsonIgnore]
- private bool IsSharedItem
- {
- get
- {
- var path = Path;
-
- if (string.IsNullOrEmpty(path))
- {
- return false;
- }
-
- return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
- }
- }
-
public override bool IsVisible(User user)
{
if (!IsSharedItem)
diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs
index c7fad5974..aefa520e7 100644
--- a/MediaBrowser.Controller/Providers/AlbumInfo.cs
+++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs
index e9181f476..4854d1a5f 100644
--- a/MediaBrowser.Controller/Providers/ArtistInfo.cs
+++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index b31270270..e6d975ffe 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Providers
public FileSystemMetadata[] GetFileSystemEntries(string path)
{
- return _cache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
+ return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
public List<FileSystemMetadata> GetFiles(string path)
@@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.Providers
_filePathCache.TryRemove(path, out _);
}
- var filePaths = _filePathCache.GetOrAdd(path, (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem);
+ var filePaths = _filePathCache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFilePaths(p).ToList(), _fileSystem);
if (sort)
{
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
index 0c932fa87..b59a03738 100644
--- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
index b1a36e102..48d627691 100644
--- a/MediaBrowser.Controller/Providers/IDirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA1819, CS1591
using System.Collections.Generic;
using MediaBrowser.Model.IO;
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
index e2dbef2bc..0d847520d 100644
--- a/MediaBrowser.Controller/Providers/IExternalId.cs
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -35,7 +33,7 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Gets the URL format string for this id.
/// </summary>
- string UrlFormatString { get; }
+ string? UrlFormatString { get; }
/// <summary>
/// Determines whether this id supports a given item type.
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 684bd9e68..9f7a76be6 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -31,6 +31,9 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Queues the refresh.
/// </summary>
+ /// <param name="itemId">Item ID.</param>
+ /// <param name="options">MetadataRefreshOptions for operation.</param>
+ /// <param name="priority">RefreshPriority for operation.</param>
void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority);
/// <summary>
@@ -85,6 +88,13 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Saves the image.
/// </summary>
+ /// <param name="item">Image to save.</param>
+ /// <param name="source">Source of image.</param>
+ /// <param name="mimeType">Mime type image.</param>
+ /// <param name="type">Type of image.</param>
+ /// <param name="imageIndex">Index of image.</param>
+ /// <param name="saveLocallyWithMedia">Option to save locally.</param>
+ /// <param name="cancellationToken">CancellationToken to use with operation.</param>
/// <returns>Task.</returns>
Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken);
@@ -93,6 +103,11 @@ namespace MediaBrowser.Controller.Providers
/// <summary>
/// Adds the metadata providers.
/// </summary>
+ /// <param name="imageProviders">Image providers to use.</param>
+ /// <param name="metadataServices">Metadata services to use.</param>
+ /// <param name="metadataProviders">Metadata providers to use.</param>
+ /// <param name="metadataSavers">Metadata savers to use.</param>
+ /// <param name="externalIds">External IDs to use.</param>
void AddParts(
IEnumerable<IImageProvider> imageProviders,
IEnumerable<IMetadataService> metadataServices,
diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
index 81a22affb..08d129a82 100644
--- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -1,6 +1,4 @@
-#nullable disable
-
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
using System;
using System.Linq;
@@ -10,6 +8,15 @@ namespace MediaBrowser.Controller.Providers
{
public class ImageRefreshOptions
{
+ public ImageRefreshOptions(IDirectoryService directoryService)
+ {
+ ImageRefreshMode = MetadataRefreshMode.Default;
+ DirectoryService = directoryService;
+
+ ReplaceImages = Array.Empty<ImageType>();
+ IsAutomated = true;
+ }
+
public MetadataRefreshMode ImageRefreshMode { get; set; }
public IDirectoryService DirectoryService { get; private set; }
@@ -20,15 +27,6 @@ namespace MediaBrowser.Controller.Providers
public bool IsAutomated { get; set; }
- public ImageRefreshOptions(IDirectoryService directoryService)
- {
- ImageRefreshMode = MetadataRefreshMode.Default;
- DirectoryService = directoryService;
-
- ReplaceImages = Array.Empty<ImageType>();
- IsAutomated = true;
- }
-
public bool IsReplacingImage(ImageType type)
{
return ImageRefreshMode == MetadataRefreshMode.FullRefresh &&
diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs
index b8dd416a2..3a97127ea 100644
--- a/MediaBrowser.Controller/Providers/ItemInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
index 2fd89e3bb..460f4e500 100644
--- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Providers
public string Name { get; set; }
/// <summary>
- /// Gets or sets the original title
+ /// Gets or sets the original title.
/// </summary>
/// <value>The original title of the item.</value>
public string OriginalTitle { get; set; }
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 2cf536779..90fd6e269 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -1,9 +1,10 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1819, CS1591
using System;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Providers;
@@ -58,7 +59,7 @@ namespace MediaBrowser.Controller.Providers
{
if (RefreshPaths != null && RefreshPaths.Length > 0)
{
- return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
return true;
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index 7ec1eefcd..2085ae4ad 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -1,6 +1,6 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CA1002, CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs
index 7e39bc37a..1edceb0e4 100644
--- a/MediaBrowser.Controller/Providers/SeasonInfo.cs
+++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CA2227, CS1591
using System;
using System.Collections.Generic;
diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs
index c90717a2e..4b64a8a98 100644
--- a/MediaBrowser.Controller/Providers/SongInfo.cs
+++ b/MediaBrowser.Controller/Providers/SongInfo.cs
@@ -9,16 +9,16 @@ namespace MediaBrowser.Controller.Providers
{
public class SongInfo : ItemLookupInfo
{
- public IReadOnlyList<string> AlbumArtists { get; set; }
-
- public string Album { get; set; }
-
- public IReadOnlyList<string> Artists { get; set; }
-
public SongInfo()
{
Artists = Array.Empty<string>();
AlbumArtists = Array.Empty<string>();
}
+
+ public IReadOnlyList<string> AlbumArtists { get; set; }
+
+ public string Album { get; set; }
+
+ public IReadOnlyList<string> Artists { get; set; }
}
}
diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
index c4e709c24..ec3706773 100644
--- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
+++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs
@@ -1,6 +1,7 @@
-#nullable disable
-
using System;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Model.QuickConnect;
namespace MediaBrowser.Controller.QuickConnect
@@ -11,46 +12,16 @@ namespace MediaBrowser.Controller.QuickConnect
public interface IQuickConnect
{
/// <summary>
- /// Gets or sets the length of user facing codes.
- /// </summary>
- int CodeLength { get; set; }
-
- /// <summary>
- /// Gets or sets the name of internal access tokens.
- /// </summary>
- string TokenName { get; set; }
-
- /// <summary>
- /// Gets the current state of quick connect.
- /// </summary>
- QuickConnectState State { get; }
-
- /// <summary>
- /// Gets or sets the time (in minutes) before quick connect will automatically deactivate.
- /// </summary>
- int Timeout { get; set; }
-
- /// <summary>
- /// Assert that quick connect is currently active and throws an exception if it is not.
+ /// Gets a value indicating whether quick connect is enabled or not.
/// </summary>
- void AssertActive();
-
- /// <summary>
- /// Temporarily activates quick connect for a short amount of time.
- /// </summary>
- void Activate();
-
- /// <summary>
- /// Changes the state of quick connect.
- /// </summary>
- /// <param name="newState">New state to change to.</param>
- void SetState(QuickConnectState newState);
+ bool IsEnabled { get; }
/// <summary>
/// Initiates a new quick connect request.
/// </summary>
+ /// <param name="authorizationInfo">The initiator authorization info.</param>
/// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
- QuickConnectResult TryConnect();
+ QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo);
/// <summary>
/// Checks the status of an individual request.
@@ -65,25 +36,13 @@ namespace MediaBrowser.Controller.QuickConnect
/// <param name="userId">User id.</param>
/// <param name="code">Identifying code for the request.</param>
/// <returns>A boolean indicating if the authorization completed successfully.</returns>
- bool AuthorizeRequest(Guid userId, string code);
-
- /// <summary>
- /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.
- /// </summary>
- /// <param name="expireAll">If true, all requests will be expired.</param>
- void ExpireRequests(bool expireAll = false);
-
- /// <summary>
- /// Deletes all quick connect access tokens for the provided user.
- /// </summary>
- /// <param name="user">Guid of the user to delete tokens for.</param>
- /// <returns>A count of the deleted tokens.</returns>
- int DeleteAllDevices(Guid user);
+ Task<bool> AuthorizeRequest(Guid userId, string code);
/// <summary>
- /// Generates a short code to display to the user to uniquely identify this request.
+ /// Gets the authorized request for the secret.
/// </summary>
- /// <returns>A short, unique alphanumeric string.</returns>
- string GenerateCode();
+ /// <param name="secret">The secret.</param>
+ /// <returns>The authentication result.</returns>
+ AuthenticationResult GetAuthorizedRequest(string secret);
}
}
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index 75286eadc..b95d00aa3 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -14,17 +14,17 @@ namespace MediaBrowser.Controller.Resolvers
public interface IItemResolver
{
/// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ ResolverPriority Priority { get; }
+
+ /// <summary>
/// Resolves the path.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
BaseItem ResolvePath(ItemResolveArgs args);
-
- /// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- ResolverPriority Priority { get; }
}
public interface IMultiItemResolver
@@ -38,14 +38,14 @@ namespace MediaBrowser.Controller.Resolvers
public class MultiItemResolverResult
{
- public List<BaseItem> Items { get; set; }
-
- public List<FileSystemMetadata> ExtraFiles { get; set; }
-
public MultiItemResolverResult()
{
Items = new List<BaseItem>();
ExtraFiles = new List<FileSystemMetadata>();
}
+
+ public List<BaseItem> Items { get; set; }
+
+ public List<FileSystemMetadata> ExtraFiles { get; set; }
}
}
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
deleted file mode 100644
index 3af6a525c..000000000
--- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Controller.Security
-{
- public class AuthenticationInfoQuery
- {
- /// <summary>
- /// Gets or sets the device identifier.
- /// </summary>
- /// <value>The device identifier.</value>
- public string DeviceId { get; set; }
-
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public Guid UserId { get; set; }
-
- /// <summary>
- /// Gets or sets the access token.
- /// </summary>
- /// <value>The access token.</value>
- public string AccessToken { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance is active.
- /// </summary>
- /// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value>
- public bool? IsActive { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether this instance has user.
- /// </summary>
- /// <value><c>null</c> if [has user] contains no value, <c>true</c> if [has user]; otherwise, <c>false</c>.</value>
- public bool? HasUser { get; set; }
-
- /// <summary>
- /// Gets or sets the start index.
- /// </summary>
- /// <value>The start index.</value>
- public int? StartIndex { get; set; }
-
- /// <summary>
- /// Gets or sets the limit.
- /// </summary>
- /// <value>The limit.</value>
- public int? Limit { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
new file mode 100644
index 000000000..e3d18c8c0
--- /dev/null
+++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
@@ -0,0 +1,33 @@
+#nullable enable
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Security
+{
+ /// <summary>
+ /// Handles the retrieval and storage of API keys.
+ /// </summary>
+ public interface IAuthenticationManager
+ {
+ /// <summary>
+ /// Creates an API key.
+ /// </summary>
+ /// <param name="name">The name of the key.</param>
+ /// <returns>A task representing the creation of the key.</returns>
+ Task CreateApiKey(string name);
+
+ /// <summary>
+ /// Gets the API keys.
+ /// </summary>
+ /// <returns>A task representing the retrieval of the API keys.</returns>
+ Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys();
+
+ /// <summary>
+ /// Deletes an API key with the provided access token.
+ /// </summary>
+ /// <param name="accessToken">The access token.</param>
+ /// <returns>A task representing the deletion of the API key.</returns>
+ Task DeleteApiKey(string accessToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
deleted file mode 100644
index 1dd69ccd8..000000000
--- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Querying;
-
-namespace MediaBrowser.Controller.Security
-{
- public interface IAuthenticationRepository
- {
- /// <summary>
- /// Creates the specified information.
- /// </summary>
- /// <param name="info">The information.</param>
- /// <returns>Task.</returns>
- void Create(AuthenticationInfo info);
-
- /// <summary>
- /// Updates the specified information.
- /// </summary>
- /// <param name="info">The information.</param>
- /// <returns>Task.</returns>
- void Update(AuthenticationInfo info);
-
- /// <summary>
- /// Gets the specified query.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>QueryResult{AuthenticationInfo}.</returns>
- QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query);
-
- void Delete(AuthenticationInfo info);
-
- DeviceOptions GetDeviceOptions(string deviceId);
-
- void UpdateDeviceOptions(string deviceId, DeviceOptions options);
- }
-}
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index 6bc39d6f4..b38ee1146 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -26,6 +26,12 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
+ /// <typeparam name="T">The type of data.</typeparam>
+ /// <param name="name">Name of message type.</param>
+ /// <param name="messageId">Message ID.</param>
+ /// <param name="data">Data to send.</param>
+ /// <param name="cancellationToken">CancellationToken for operation.</param>
+ /// <returns>A task.</returns>
Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 4c3cf5ffe..c86556095 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -6,11 +6,10 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@@ -83,7 +82,8 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceName">Name of the device.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
- SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
+ /// <returns>A task containing the session information.</returns>
+ Task<SessionInfo> LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user);
/// <summary>
/// Used to report that a session controller has connected.
@@ -105,7 +105,7 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception>
Task OnPlaybackProgress(PlaybackProgressInfo info);
Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated);
@@ -115,14 +115,13 @@ namespace MediaBrowser.Controller.Session
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ArgumentNullException">Throws if an argument is null.</exception>
Task OnPlaybackStopped(PlaybackStopInfo info);
/// <summary>
/// Reports the session ended.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
- /// <returns>Task.</returns>
void ReportSessionEnded(string sessionId);
/// <summary>
@@ -158,20 +157,21 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends a SyncPlayCommand to a session.
/// </summary>
- /// <param name="session">The session.</param>
+ /// <param name="sessionId">The identifier of the session.</param>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken);
+ Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken);
/// <summary>
/// Sends a SyncPlayGroupUpdate to a session.
/// </summary>
- /// <param name="session">The session.</param>
+ /// <param name="sessionId">The identifier of the session.</param>
/// <param name="command">The group update.</param>
/// <param name="cancellationToken">The cancellation token.</param>
+ /// <typeparam name="T">Type of group.</typeparam>
/// <returns>Task.</returns>
- Task SendSyncPlayGroupUpdate<T>(SessionInfo session, GroupUpdate<T> command, CancellationToken cancellationToken);
+ Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken);
/// <summary>
/// Sends the browse command.
@@ -196,8 +196,8 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message to admin sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="name">The name.</param>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="name">Message type name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -206,18 +206,31 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message to user sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="userIds">Users to send messages to.</param>
+ /// <param name="name">Message type name.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
+ /// <summary>
+ /// Sends the message to user sessions.
+ /// </summary>
+ /// <typeparam name="T">Type of data.</typeparam>
+ /// <param name="userIds">Users to send messages to.</param>
+ /// <param name="name">Message type name.</param>
+ /// <param name="dataFn">Data function.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user device sessions.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">Type of data.</typeparam>
/// <param name="deviceId">The device identifier.</param>
- /// <param name="name">The name.</param>
+ /// <param name="name">Message type name.</param>
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -266,33 +279,13 @@ namespace MediaBrowser.Controller.Session
void ReportNowViewingItem(string sessionId, string itemId);
/// <summary>
- /// Reports the now viewing item.
- /// </summary>
- /// <param name="sessionId">The session identifier.</param>
- /// <param name="item">The item.</param>
- void ReportNowViewingItem(string sessionId, BaseItemDto item);
-
- /// <summary>
/// Authenticates the new session.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Task{SessionInfo}.</returns>
Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
- /// <summary>
- /// Authenticates a new session with quick connect.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <param name="token">Quick connect access token.</param>
- /// <returns>Task{SessionInfo}.</returns>
- Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token);
-
- /// <summary>
- /// Creates the new session.
- /// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task&lt;AuthenticationResult&gt;.</returns>
- Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request);
+ Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request);
/// <summary>
/// Reports the capabilities.
@@ -330,7 +323,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="deviceId">The device identifier.</param>
/// <param name="remoteEndpoint">The remote endpoint.</param>
/// <returns>SessionInfo.</returns>
- SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint);
+ Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint);
/// <summary>
/// Gets the session by authentication token.
@@ -340,26 +333,24 @@ namespace MediaBrowser.Controller.Session
/// <param name="remoteEndpoint">The remote endpoint.</param>
/// <param name="appVersion">The application version.</param>
/// <returns>Task&lt;SessionInfo&gt;.</returns>
- SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion);
+ Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion);
/// <summary>
/// Logouts the specified access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
- void Logout(string accessToken);
+ /// <returns>A <see cref="Task"/> representing the log out process.</returns>
+ Task Logout(string accessToken);
- void Logout(AuthenticationInfo accessToken);
+ Task Logout(Device device);
/// <summary>
/// Revokes the user tokens.
/// </summary>
- void RevokeUserTokens(Guid userId, string currentAccessToken);
-
- /// <summary>
- /// Revokes the token.
- /// </summary>
- /// <param name="id">The identifier.</param>
- void RevokeToken(string id);
+ /// <param name="userId">The user's id.</param>
+ /// <param name="currentAccessToken">The current access token.</param>
+ /// <returns>Task.</returns>
+ Task RevokeUserTokens(Guid userId, string currentAccessToken);
void CloseIfNeeded(SessionInfo session);
}
diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs
index aa6ec513f..f9c0d39dd 100644
--- a/MediaBrowser.Controller/Sorting/SortExtensions.cs
+++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs
@@ -1,16 +1,15 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Extensions;
namespace MediaBrowser.Controller.Sorting
{
public static class SortExtensions
{
- private static readonly AlphanumComparator _comparer = new AlphanumComparator();
+ private static readonly AlphanumericComparator _comparer = new AlphanumericComparator();
public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName)
{
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index 9e661cbe4..52aa44024 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -28,10 +28,17 @@ namespace MediaBrowser.Controller.Subtitles
/// <summary>
/// Searches the subtitles.
/// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="language">Subtitle language.</param>
+ /// <param name="isPerfectMatch">Require perfect match.</param>
+ /// <param name="isAutomated">Request is automated.</param>
+ /// <param name="cancellationToken">CancellationToken to use for the operation.</param>
+ /// <returns>Subtitles, wrapped in task.</returns>
Task<RemoteSubtitleInfo[]> SearchSubtitles(
Video video,
string language,
bool? isPerfectMatch,
+ bool isAutomated,
CancellationToken cancellationToken);
/// <summary>
@@ -47,11 +54,20 @@ namespace MediaBrowser.Controller.Subtitles
/// <summary>
/// Downloads the subtitles.
/// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="subtitleId">Subtitle ID.</param>
+ /// <param name="cancellationToken">CancellationToken to use for the operation.</param>
+ /// <returns>A task.</returns>
Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken);
/// <summary>
/// Downloads the subtitles.
/// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="libraryOptions">Library options to use.</param>
+ /// <param name="subtitleId">Subtitle ID.</param>
+ /// <param name="cancellationToken">CancellationToken to use for the operation.</param>
+ /// <returns>A task.</returns>
Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken);
/// <summary>
@@ -73,11 +89,16 @@ namespace MediaBrowser.Controller.Subtitles
/// <summary>
/// Deletes the subtitles.
/// </summary>
+ /// <param name="item">Media item.</param>
+ /// <param name="index">Subtitle index.</param>
+ /// <returns>A task.</returns>
Task DeleteSubtitles(BaseItem item, int index);
/// <summary>
/// Gets the providers.
/// </summary>
+ /// <param name="item">The media item.</param>
+ /// <returns>Subtitles providers.</returns>
SubtitleProviderInfo[] GetSupportedProviders(BaseItem item);
}
}
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
index 0f7c47e76..ef052237a 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
@@ -11,6 +11,15 @@ namespace MediaBrowser.Controller.Subtitles
{
public class SubtitleSearchRequest : IHasProviderIds
{
+ public SubtitleSearchRequest()
+ {
+ SearchAllProviders = true;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ DisabledSubtitleFetchers = Array.Empty<string>();
+ SubtitleFetcherOrder = Array.Empty<string>();
+ }
+
public string Language { get; set; }
public string TwoLetterISOLanguageName { get; set; }
@@ -43,13 +52,6 @@ namespace MediaBrowser.Controller.Subtitles
public string[] SubtitleFetcherOrder { get; set; }
- public SubtitleSearchRequest()
- {
- SearchAllProviders = true;
- ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- DisabledSubtitleFetchers = Array.Empty<string>();
- SubtitleFetcherOrder = Array.Empty<string>();
- }
+ public bool IsAutomated { get; set; }
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
index 7e7e759a5..b973672c4 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
@@ -1,5 +1,6 @@
#nullable disable
+using System;
using MediaBrowser.Controller.Session;
namespace MediaBrowser.Controller.SyncPlay
@@ -15,14 +16,28 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public GroupMember(SessionInfo session)
{
- Session = session;
+ SessionId = session.Id;
+ UserId = session.UserId;
+ UserName = session.UserName;
}
/// <summary>
- /// Gets the session.
+ /// Gets the identifier of the session.
/// </summary>
- /// <value>The session.</value>
- public SessionInfo Session { get; }
+ /// <value>The session identifier.</value>
+ public string SessionId { get; }
+
+ /// <summary>
+ /// Gets the identifier of the user.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; }
+
+ /// <summary>
+ /// Gets the username.
+ /// </summary>
+ /// <value>The username.</value>
+ public string UserName { get; }
/// <summary>
/// Gets or sets the ping, in milliseconds.
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
index 91a13fb28..51c95a1bb 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs
@@ -68,7 +68,16 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
/// <inheritdoc />
public virtual void HandleRequest(RemoveFromPlaylistGroupRequest request, IGroupStateContext context, GroupStateType prevState, SessionInfo session, CancellationToken cancellationToken)
{
- var playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds);
+ bool playingItemRemoved;
+ if (request.ClearPlaylist)
+ {
+ context.ClearPlayQueue(request.ClearPlayingItem);
+ playingItemRemoved = request.ClearPlayingItem;
+ }
+ else
+ {
+ playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds);
+ }
var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems);
var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
index b9786ddb0..2523ec709 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs
@@ -18,18 +18,12 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
public class PausedGroupState : AbstractGroupState
{
/// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<PausedGroupState> _logger;
-
- /// <summary>
/// Initializes a new instance of the <see cref="PausedGroupState"/> class.
/// </summary>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public PausedGroupState(ILoggerFactory loggerFactory)
: base(loggerFactory)
{
- _logger = LoggerFactory.CreateLogger<PausedGroupState>();
}
/// <inheritdoc />
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
index cb1cadf0b..4f29ca1c6 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs
@@ -18,18 +18,12 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
public class PlayingGroupState : AbstractGroupState
{
/// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger<PlayingGroupState> _logger;
-
- /// <summary>
/// Initializes a new instance of the <see cref="PlayingGroupState"/> class.
/// </summary>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public PlayingGroupState(ILoggerFactory loggerFactory)
: base(loggerFactory)
{
- _logger = LoggerFactory.CreateLogger<PlayingGroupState>();
}
/// <inheritdoc />
diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
index de26c7d9e..d2de22450 100644
--- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
+++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs
@@ -163,6 +163,12 @@ namespace MediaBrowser.Controller.SyncPlay
bool SetPlayingItem(Guid playlistItemId);
/// <summary>
+ /// Clears the play queue.
+ /// </summary>
+ /// <param name="clearPlayingItem">Whether to remove the playing item as well.</param>
+ void ClearPlayQueue(bool clearPlayingItem);
+
+ /// <summary>
/// Removes items from the play queue.
/// </summary>
/// <param name="playlistItemIds">The items to remove.</param>
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
index 689145293..2f38d6adc 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequests/RemoveFromPlaylistGroupRequest.cs
@@ -17,9 +17,13 @@ namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
/// Initializes a new instance of the <see cref="RemoveFromPlaylistGroupRequest"/> class.
/// </summary>
/// <param name="items">The playlist ids of the items to remove.</param>
- public RemoveFromPlaylistGroupRequest(IReadOnlyList<Guid> items)
+ /// <param name="clearPlaylist">Whether to clear the entire playlist. The items list will be ignored.</param>
+ /// <param name="clearPlayingItem">Whether to remove the playing item as well. Used only when clearing the playlist.</param>
+ public RemoveFromPlaylistGroupRequest(IReadOnlyList<Guid> items, bool clearPlaylist = false, bool clearPlayingItem = false)
{
PlaylistItemIds = items ?? Array.Empty<Guid>();
+ ClearPlaylist = clearPlaylist;
+ ClearPlayingItem = clearPlayingItem;
}
/// <summary>
@@ -28,6 +32,18 @@ namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
/// <value>The playlist identifiers ot the items.</value>
public IReadOnlyList<Guid> PlaylistItemIds { get; }
+ /// <summary>
+ /// Gets a value indicating whether the entire playlist should be cleared.
+ /// </summary>
+ /// <value>Whether the entire playlist should be cleared.</value>
+ public bool ClearPlaylist { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the playing item should be removed as well.
+ /// </summary>
+ /// <value>Whether the playing item should be removed as well.</value>
+ public bool ClearPlayingItem { get; }
+
/// <inheritdoc />
public override PlaybackRequestType Action { get; } = PlaybackRequestType.RemoveFromPlaylist;
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index b8ae9f3ff..f49876cca 100644
--- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.SyncPlay.Queue
@@ -19,10 +20,16 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
private const int NoPlayingItemIndex = -1;
/// <summary>
- /// Random number generator used to shuffle lists.
+ /// The sorted playlist.
/// </summary>
- /// <value>The random number generator.</value>
- private readonly Random _randomNumberGenerator = new Random();
+ /// <value>The sorted playlist, or play queue of the group.</value>
+ private List<QueueItem> _sortedPlaylist = new List<QueueItem>();
+
+ /// <summary>
+ /// The shuffled playlist.
+ /// </summary>
+ /// <value>The shuffled playlist, or play queue of the group.</value>
+ private List<QueueItem> _shuffledPlaylist = new List<QueueItem>();
/// <summary>
/// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
@@ -57,18 +64,6 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
public GroupRepeatMode RepeatMode { get; private set; } = GroupRepeatMode.RepeatNone;
/// <summary>
- /// Gets or sets the sorted playlist.
- /// </summary>
- /// <value>The sorted playlist, or play queue of the group.</value>
- private List<QueueItem> SortedPlaylist { get; set; } = new List<QueueItem>();
-
- /// <summary>
- /// Gets or sets the shuffled playlist.
- /// </summary>
- /// <value>The shuffled playlist, or play queue of the group.</value>
- private List<QueueItem> ShuffledPlaylist { get; set; } = new List<QueueItem>();
-
- /// <summary>
/// Checks if an item is playing.
/// </summary>
/// <returns><c>true</c> if an item is playing; <c>false</c> otherwise.</returns>
@@ -92,14 +87,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// <param name="items">The new items of the playlist.</param>
public void SetPlaylist(IReadOnlyList<Guid> items)
{
- SortedPlaylist.Clear();
- ShuffledPlaylist.Clear();
+ _sortedPlaylist.Clear();
+ _shuffledPlaylist.Clear();
- SortedPlaylist = CreateQueueItemsFromArray(items);
+ _sortedPlaylist = CreateQueueItemsFromArray(items);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
- Shuffle(ShuffledPlaylist);
+ _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist.Shuffle();
}
PlayingItemIndex = NoPlayingItemIndex;
@@ -114,10 +109,10 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
var newItems = CreateQueueItemsFromArray(items);
- SortedPlaylist.AddRange(newItems);
+ _sortedPlaylist.AddRange(newItems);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- ShuffledPlaylist.AddRange(newItems);
+ _shuffledPlaylist.AddRange(newItems);
}
LastChange = DateTime.UtcNow;
@@ -130,26 +125,26 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (PlayingItemIndex == NoPlayingItemIndex)
{
- ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
- Shuffle(ShuffledPlaylist);
+ _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist.Shuffle();
}
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
{
// First time shuffle.
- var playingItem = SortedPlaylist[PlayingItemIndex];
- ShuffledPlaylist = new List<QueueItem>(SortedPlaylist);
- ShuffledPlaylist.RemoveAt(PlayingItemIndex);
- Shuffle(ShuffledPlaylist);
- ShuffledPlaylist.Insert(0, playingItem);
+ var playingItem = _sortedPlaylist[PlayingItemIndex];
+ _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist.RemoveAt(PlayingItemIndex);
+ _shuffledPlaylist.Shuffle();
+ _shuffledPlaylist.Insert(0, playingItem);
PlayingItemIndex = 0;
}
else
{
// Re-shuffle playlist.
- var playingItem = ShuffledPlaylist[PlayingItemIndex];
- ShuffledPlaylist.RemoveAt(PlayingItemIndex);
- Shuffle(ShuffledPlaylist);
- ShuffledPlaylist.Insert(0, playingItem);
+ var playingItem = _shuffledPlaylist[PlayingItemIndex];
+ _shuffledPlaylist.RemoveAt(PlayingItemIndex);
+ _shuffledPlaylist.Shuffle();
+ _shuffledPlaylist.Insert(0, playingItem);
PlayingItemIndex = 0;
}
@@ -164,11 +159,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (PlayingItemIndex != NoPlayingItemIndex)
{
- var playingItem = ShuffledPlaylist[PlayingItemIndex];
- PlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ var playingItem = _shuffledPlaylist[PlayingItemIndex];
+ PlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);
}
- ShuffledPlaylist.Clear();
+ _shuffledPlaylist.Clear();
ShuffleMode = GroupShuffleMode.Sorted;
LastChange = DateTime.UtcNow;
@@ -181,16 +176,16 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
public void ClearPlaylist(bool clearPlayingItem)
{
var playingItem = GetPlayingItem();
- SortedPlaylist.Clear();
- ShuffledPlaylist.Clear();
+ _sortedPlaylist.Clear();
+ _shuffledPlaylist.Clear();
LastChange = DateTime.UtcNow;
if (!clearPlayingItem && playingItem != null)
{
- SortedPlaylist.Add(playingItem);
+ _sortedPlaylist.Add(playingItem);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- ShuffledPlaylist.Add(playingItem);
+ _shuffledPlaylist.Add(playingItem);
}
PlayingItemIndex = 0;
@@ -212,14 +207,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
var playingItem = GetPlayingItem();
- var sortedPlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ var sortedPlayingItemIndex = _sortedPlaylist.IndexOf(playingItem);
// Append items to sorted and shuffled playlist as they are.
- SortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems);
- ShuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ _sortedPlaylist.InsertRange(sortedPlayingItemIndex + 1, newItems);
+ _shuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
}
else
{
- SortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ _sortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
}
LastChange = DateTime.UtcNow;
@@ -298,8 +293,8 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
var playingItem = GetPlayingItem();
- SortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
- ShuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
+ _sortedPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
+ _shuffledPlaylist.RemoveAll(item => playlistItemIds.Contains(item.PlaylistItemId));
LastChange = DateTime.UtcNow;
@@ -313,7 +308,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
// Was first element, picking next if available.
// Default to no playing item otherwise.
- PlayingItemIndex = SortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
+ PlayingItemIndex = _sortedPlaylist.Count > 0 ? 0 : NoPlayingItemIndex;
}
return true;
@@ -363,8 +358,8 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// </summary>
public void Reset()
{
- SortedPlaylist.Clear();
- ShuffledPlaylist.Clear();
+ _sortedPlaylist.Clear();
+ _shuffledPlaylist.Clear();
PlayingItemIndex = NoPlayingItemIndex;
ShuffleMode = GroupShuffleMode.Sorted;
RepeatMode = GroupRepeatMode.RepeatNone;
@@ -460,7 +455,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
PlayingItemIndex++;
- if (PlayingItemIndex >= SortedPlaylist.Count)
+ if (PlayingItemIndex >= _sortedPlaylist.Count)
{
if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
{
@@ -468,7 +463,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
else
{
- PlayingItemIndex = SortedPlaylist.Count - 1;
+ PlayingItemIndex = _sortedPlaylist.Count - 1;
return false;
}
}
@@ -494,7 +489,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
{
- PlayingItemIndex = SortedPlaylist.Count - 1;
+ PlayingItemIndex = _sortedPlaylist.Count - 1;
}
else
{
@@ -508,23 +503,6 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
/// <summary>
- /// Shuffles a given list.
- /// </summary>
- /// <param name="list">The list to shuffle.</param>
- private void Shuffle<T>(IList<T> list)
- {
- int n = list.Count;
- while (n > 1)
- {
- n--;
- int k = _randomNumberGenerator.Next(n + 1);
- T value = list[k];
- list[k] = list[n];
- list[n] = value;
- }
- }
-
- /// <summary>
/// Creates a list from the array of items. Each item is given an unique playlist identifier.
/// </summary>
/// <returns>The list of queue items.</returns>
@@ -548,11 +526,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- return ShuffledPlaylist;
+ return _shuffledPlaylist;
}
else
{
- return SortedPlaylist;
+ return _sortedPlaylist;
}
}
@@ -568,11 +546,11 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
}
else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- return ShuffledPlaylist[PlayingItemIndex];
+ return _shuffledPlaylist[PlayingItemIndex];
}
else
{
- return SortedPlaylist[PlayingItemIndex];
+ return _sortedPlaylist[PlayingItemIndex];
}
}
}
diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
index fdba64c4a..dc13bf4f6 100644
--- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
index 10d691b3e..d3fa41bcd 100644
--- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
@@ -15,22 +14,18 @@ namespace MediaBrowser.LocalMetadata.Images
/// </summary>
public class InternalMetadataFolderImageProvider : ILocalImageProvider, IHasOrder
{
- private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly ILogger<InternalMetadataFolderImageProvider> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="InternalMetadataFolderImageProvider"/> class.
/// </summary>
- /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{InternalMetadataFolderImageProvider}"/> interface.</param>
public InternalMetadataFolderImageProvider(
- IServerConfigurationManager config,
IFileSystem fileSystem,
ILogger<InternalMetadataFolderImageProvider> logger)
{
- _config = config;
_fileSystem = fileSystem;
_logger = logger;
}
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index db9c65a60..7dc6149f4 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -60,8 +61,6 @@ namespace MediaBrowser.LocalMetadata.Images
private readonly IFileSystem _fileSystem;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
/// <summary>
/// Initializes a new instance of the <see cref="LocalImageProvider"/> class.
/// </summary>
@@ -119,16 +118,10 @@ namespace MediaBrowser.LocalMetadata.Images
return Enumerable.Empty<FileSystemMetadata>();
}
- if (includeDirectories)
- {
- return directoryService.GetFileSystemEntries(path)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase) || i.IsDirectory)
-
- .OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
- }
-
- return directoryService.GetFiles(path)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase))
+ return directoryService.GetFileSystemEntries(path)
+ .Where(i =>
+ (includeDirectories && i.IsDirectory)
+ || BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparison.OrdinalIgnoreCase))
.OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
}
@@ -258,11 +251,6 @@ namespace MediaBrowser.LocalMetadata.Images
{
PopulateBackdrops(item, images, files, imagePrefix, isInMixedFolder);
}
-
- if (item is IHasScreenshots)
- {
- PopulateScreenshots(images, files, imagePrefix, isInMixedFolder);
- }
}
private void PopulatePrimaryImages(BaseItem item, List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
@@ -283,7 +271,7 @@ namespace MediaBrowser.LocalMetadata.Images
{
imageFileNames = _seriesImageFileNames;
}
- else if (item is Video && !(item is Episode))
+ else if (item is Video && item is not Episode)
{
imageFileNames = _videoImageFileNames;
}
@@ -365,11 +353,6 @@ namespace MediaBrowser.LocalMetadata.Images
}));
}
- private void PopulateScreenshots(List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
- {
- PopulateBackdrops(images, files, imagePrefix, "screenshot", "screenshot", isInMixedFolder, ImageType.Screenshot);
- }
-
private void PopulateBackdrops(List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, bool isInMixedFolder, ImageType type)
{
AddImage(files, images, imagePrefix + firstFileName, type);
@@ -434,7 +417,7 @@ namespace MediaBrowser.LocalMetadata.Images
var seasonMarker = seasonNumber.Value == 0
? "-specials"
- : seasonNumber.Value.ToString("00", _usCulture);
+ : seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
// Get this one directly from the file system since we have to go up a level
if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index eb2077a5f..a3db717b9 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -11,13 +11,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index 32e5ac761..777fe6774 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -20,8 +21,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
public class BaseItemXmlParser<T>
where T : BaseItem
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private Dictionary<string, string>? _validProviderIds;
/// <summary>
@@ -81,7 +80,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
var id = info.Key + "Id";
if (!_validProviderIds.ContainsKey(id))
{
- _validProviderIds.Add(id, info.Key!);
+ _validProviderIds.Add(id, info.Key);
}
}
@@ -144,13 +143,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParse(val, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
{
- item.DateCreated = added.ToUniversalTime();
+ item.DateCreated = added;
}
else
{
- Logger.LogWarning("Invalid Added value found: " + val);
+ Logger.LogWarning("Invalid Added value found: {Value}", val);
}
}
@@ -179,7 +178,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrEmpty(text))
{
- if (float.TryParse(text, NumberStyles.Any, _usCulture, out var value))
+ if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
item.CriticRating = value;
}
@@ -331,7 +330,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(text))
{
- if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out var runtime))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
@@ -413,7 +412,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
var actors = reader.ReadInnerXml();
- if (actors.Contains("<", StringComparison.Ordinal))
+ if (actors.Contains('<', StringComparison.Ordinal))
{
// This is one of the mis-named "Actors" full nodes created by MB2
// Create a reader and pass it to the persons node processor
@@ -534,9 +533,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(firstAired))
{
- if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var airDate) && airDate.Year > 1850)
+ if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
{
- item.PremiereDate = airDate.ToUniversalTime();
+ item.PremiereDate = airDate;
item.ProductionYear = airDate.Year;
}
}
@@ -551,9 +550,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(firstAired))
{
- if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var airDate) && airDate.Year > 1850)
+ if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
{
- item.EndDate = airDate.ToUniversalTime();
+ item.EndDate = airDate;
}
}
@@ -750,46 +749,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
item.Shares = list.ToArray();
}
- private Share GetShareFromNode(XmlReader reader)
- {
- var share = new Share();
-
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "UserId":
- {
- share.UserId = reader.ReadElementContentAsString();
- break;
- }
-
- case "CanEdit":
- {
- share.CanEdit = string.Equals(reader.ReadElementContentAsString(), true.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return share;
- }
-
private void FetchFromCountriesNode(XmlReader reader)
{
reader.MoveToContent();
@@ -1101,7 +1060,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Name":
- name = reader.ReadElementContentAsString() ?? string.Empty;
+ name = reader.ReadElementContentAsString();
break;
case "Type":
@@ -1134,7 +1093,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var intVal))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
@@ -1270,8 +1229,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
/// <returns>IEnumerable{System.String}.</returns>
private IEnumerable<string> SplitNames(string value)
{
- value ??= string.Empty;
-
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
var separator = !value.Contains('|', StringComparison.Ordinal)
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index 98ed3dcf7..1a8b5bb4e 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -25,24 +25,18 @@ namespace MediaBrowser.LocalMetadata.Savers
/// </summary>
public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
/// <summary>
/// Initializes a new instance of the <see cref="BaseXmlSaver"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{BaseXmlSaver}"/> interface.</param>
- protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BaseXmlSaver> logger)
+ protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger<BaseXmlSaver> logger)
{
FileSystem = fileSystem;
ConfigurationManager = configurationManager;
LibraryManager = libraryManager;
- UserManager = userManager;
- UserDataManager = userDataManager;
Logger = logger;
}
@@ -62,16 +56,6 @@ namespace MediaBrowser.LocalMetadata.Savers
protected ILibraryManager LibraryManager { get; private set; }
/// <summary>
- /// Gets the user manager.
- /// </summary>
- protected IUserManager UserManager { get; private set; }
-
- /// <summary>
- /// Gets the user data manager.
- /// </summary>
- protected IUserDataManager UserDataManager { get; private set; }
-
- /// <summary>
/// Gets the logger.
/// </summary>
protected ILogger<BaseXmlSaver> Logger { get; private set; }
@@ -129,11 +113,19 @@ namespace MediaBrowser.LocalMetadata.Savers
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ var fileStreamOptions = new FileStreamOptions()
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.Write,
+ Share = FileShare.None,
+ PreallocationSize = stream.Length
+ };
+
+ using (var filestream = new FileStream(path, fileStreamOptions))
{
stream.CopyTo(filestream);
}
@@ -152,7 +144,7 @@ namespace MediaBrowser.LocalMetadata.Savers
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error setting hidden attribute on {path}", path);
+ Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
}
}
@@ -219,7 +211,7 @@ namespace MediaBrowser.LocalMetadata.Savers
if (item.CriticRating.HasValue)
{
- writer.WriteElementString("CriticRating", item.CriticRating.Value.ToString(_usCulture));
+ writer.WriteElementString("CriticRating", item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(item.Overview))
@@ -237,7 +229,7 @@ namespace MediaBrowser.LocalMetadata.Savers
writer.WriteElementString("CustomRating", item.CustomRating);
}
- if (!string.IsNullOrEmpty(item.Name) && !(item is Episode))
+ if (!string.IsNullOrEmpty(item.Name) && item is not Episode)
{
writer.WriteElementString("LocalTitle", item.Name);
}
@@ -254,7 +246,7 @@ namespace MediaBrowser.LocalMetadata.Savers
{
writer.WriteElementString("BirthDate", item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
- else if (!(item is Episode))
+ else if (item is not Episode)
{
writer.WriteElementString("PremiereDate", item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
@@ -266,7 +258,7 @@ namespace MediaBrowser.LocalMetadata.Savers
{
writer.WriteElementString("DeathDate", item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
- else if (!(item is Episode))
+ else if (item is not Episode)
{
writer.WriteElementString("EndDate", item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
@@ -303,12 +295,12 @@ namespace MediaBrowser.LocalMetadata.Savers
if (item.CommunityRating.HasValue)
{
- writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(_usCulture));
+ writer.WriteElementString("Rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
}
- if (item.ProductionYear.HasValue && !(item is Person))
+ if (item.ProductionYear.HasValue && item is not Person)
{
- writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(_usCulture));
+ writer.WriteElementString("ProductionYear", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
}
if (item is IHasAspectRatio hasAspectRatio)
@@ -334,9 +326,9 @@ namespace MediaBrowser.LocalMetadata.Savers
if (runTimeTicks.HasValue)
{
- var timespan = TimeSpan.FromTicks(runTimeTicks!.Value);
+ var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
- writer.WriteElementString("RunningTime", Math.Floor(timespan.TotalMinutes).ToString(_usCulture));
+ writer.WriteElementString("RunningTime", Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
}
if (item.ProviderIds != null)
@@ -409,7 +401,7 @@ namespace MediaBrowser.LocalMetadata.Savers
if (person.SortOrder.HasValue)
{
- writer.WriteElementString("SortOrder", person.SortOrder.Value.ToString(_usCulture));
+ writer.WriteElementString("SortOrder", person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
}
writer.WriteEndElement();
diff --git a/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs
index b08387b0c..8a5da95bf 100644
--- a/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs
@@ -20,11 +20,9 @@ namespace MediaBrowser.LocalMetadata.Savers
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{BoxSetXmlSaver}"/> interface.</param>
- public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<BoxSetXmlSaver> logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
+ public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger<BoxSetXmlSaver> logger)
+ : base(fileSystem, configurationManager, libraryManager, logger)
{
}
diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
index c2f106423..76252bc09 100644
--- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
@@ -25,11 +25,9 @@ namespace MediaBrowser.LocalMetadata.Savers
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistXmlSaver}"/> interface.</param>
- public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger<PlaylistXmlSaver> logger)
- : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger)
+ public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger<PlaylistXmlSaver> logger)
+ : base(fileSystem, configurationManager, libraryManager, logger)
{
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index a0ec3bd90..9ebc0d0cf 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -89,7 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
CancellationToken cancellationToken)
{
var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
- return File.OpenRead(attachmentPath);
+ return AsyncFile.OpenRead(attachmentPath);
}
private async Task<string> GetReadableFile(
@@ -223,11 +223,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
if (failed)
{
- var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}";
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
- _logger.LogError(msg);
-
- throw new InvalidOperationException(msg);
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index e86e518be..409379c35 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -75,7 +75,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
x => new BdInfoFileInfo(x));
}
- public static IDirectoryInfo FromFileSystemPath(Model.IO.IFileSystem fs, string path)
+ public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
{
return new BdInfoDirectoryInfo(fs, path);
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
index 41143c259..d55688e3d 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
public bool IsDir => _impl.IsDirectory;
- public System.IO.Stream OpenRead()
+ public Stream OpenRead()
{
return new FileStream(
FullName,
@@ -33,9 +33,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo
FileShare.Read);
}
- public System.IO.StreamReader OpenText()
+ public StreamReader OpenText()
{
- return new System.IO.StreamReader(OpenRead());
+ return new StreamReader(OpenRead());
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index f782e65bd..60a2d39e5 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -12,8 +12,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
public class EncoderValidator
{
- private const string DefaultEncoderPath = "ffmpeg";
-
private static readonly string[] _requiredDecoders = new[]
{
"h264",
@@ -89,6 +87,24 @@ namespace MediaBrowser.MediaEncoding.Encoder
"hevc_videotoolbox"
};
+ private static readonly string[] _requiredFilters = new[]
+ {
+ "scale_cuda",
+ "yadif_cuda",
+ "hwupload_cuda",
+ "overlay_cuda",
+ "tonemap_cuda",
+ "tonemap_opencl",
+ "tonemap_vaapi",
+ };
+
+ private static readonly IReadOnlyDictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
+ {
+ { 0, new string[] { "scale_cuda", "Output format (default \"same\")" } },
+ { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } },
+ { 2, new string[] { "tonemap_opencl", "bt2390" } }
+ };
+
// These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below
private static readonly IReadOnlyDictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
{
@@ -106,7 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly string _encoderPath;
- public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath)
+ public EncoderValidator(ILogger logger, string encoderPath)
{
_logger = logger;
_encoderPath = encoderPath;
@@ -156,7 +172,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// Work out what the version under test is
- var version = GetFFmpegVersion(versionOutput);
+ var version = GetFFmpegVersionInternal(versionOutput);
_logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown");
@@ -200,6 +216,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
public IEnumerable<string> GetHwaccels() => GetHwaccelTypes();
+ public IEnumerable<string> GetFilters() => GetFFmpegFilters();
+
+ public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
+
+ public Version? GetFFmpegVersion()
+ {
+ string output;
+ try
+ {
+ output = GetProcessOutput(_encoderPath, "-version");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error validating encoder");
+ return null;
+ }
+
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ _logger.LogError("FFmpeg validation: The process returned no result");
+ return null;
+ }
+
+ _logger.LogDebug("ffmpeg output: {Output}", output);
+
+ return GetFFmpegVersionInternal(output);
+ }
+
/// <summary>
/// Using the output from "ffmpeg -version" work out the FFmpeg version.
/// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy
@@ -208,7 +252,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
/// <param name="output">The output from "ffmpeg -version".</param>
/// <returns>The FFmpeg version.</returns>
- internal Version? GetFFmpegVersion(string output)
+ internal Version? GetFFmpegVersionInternal(string output)
{
// For pre-built binaries the FFmpeg version should be mentioned at the very start of the output
var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)");
@@ -297,9 +341,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
return found;
}
- public bool CheckFilter(string filter, string option)
+ public bool CheckFilterWithOption(string filter, string option)
{
- if (string.IsNullOrEmpty(filter))
+ if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
{
return false;
}
@@ -317,11 +361,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (output.Contains("Filter " + filter, StringComparison.Ordinal))
{
- if (string.IsNullOrEmpty(option))
- {
- return true;
- }
-
return output.Contains(option, StringComparison.Ordinal);
}
@@ -362,6 +401,49 @@ namespace MediaBrowser.MediaEncoding.Encoder
return found;
}
+ private IEnumerable<string> GetFFmpegFilters()
+ {
+ string output;
+ try
+ {
+ output = GetProcessOutput(_encoderPath, "-filters");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error detecting available filters");
+ return Enumerable.Empty<string>();
+ }
+
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ return Enumerable.Empty<string>();
+ }
+
+ var found = Regex
+ .Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
+ .Cast<Match>()
+ .Select(x => x.Groups["filter"].Value)
+ .Where(x => _requiredFilters.Contains(x));
+
+ _logger.LogInformation("Available filters: {Filters}", found);
+
+ return found;
+ }
+
+ private IDictionary<int, bool> GetFFmpegFiltersWithOption()
+ {
+ IDictionary<int, bool> dict = new Dictionary<int, bool>();
+ for (int i = 0; i < _filterOptionsDict.Count; i++)
+ {
+ if (_filterOptionsDict.TryGetValue(i, out var val) && val.Length == 2)
+ {
+ dict.Add(i, CheckFilterWithOption(val[0], val[1]));
+ }
+ }
+
+ return dict;
+ }
+
private string GetProcessOutput(string path, string arguments)
{
using (var process = new Process()
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 3af618af8..1c97a1982 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -11,19 +11,20 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
@@ -44,11 +45,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
internal const int DefaultHdrImageExtractionTimeout = 20000;
- /// <summary>
- /// The us culture.
- /// </summary>
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
private readonly ILogger<MediaEncoder> _logger;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
@@ -66,10 +62,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
private List<string> _encoders = new List<string>();
private List<string> _decoders = new List<string>();
private List<string> _hwaccels = new List<string>();
+ private List<string> _filters = new List<string>();
+ private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
+ private Version _ffmpegVersion = null;
private string _ffmpegPath = string.Empty;
private string _ffprobePath;
- private int threads;
+ private int _threads;
public MediaEncoder(
ILogger<MediaEncoder> logger,
@@ -89,9 +88,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public string EncoderPath => _ffmpegPath;
- /// <inheritdoc />
- public FFmpegLocation EncoderLocation { get; private set; }
-
/// <summary>
/// Run at startup or if the user removes a Custom path from transcode page.
/// Sets global variables FFmpegPath.
@@ -100,20 +96,23 @@ namespace MediaBrowser.MediaEncoding.Encoder
public void SetFFmpegPath()
{
// 1) Custom path stored in config/encoding xml file under tag <EncoderAppPath> takes precedence
- if (!ValidatePath(_configurationManager.GetEncodingOptions().EncoderAppPath, FFmpegLocation.Custom))
+ var ffmpegPath = _configurationManager.GetEncodingOptions().EncoderAppPath;
+ if (string.IsNullOrEmpty(ffmpegPath))
{
// 2) Check if the --ffmpeg CLI switch has been given
- if (!ValidatePath(_startupOptionFFmpegPath, FFmpegLocation.SetByArgument))
+ ffmpegPath = _startupOptionFFmpegPath;
+ if (string.IsNullOrEmpty(ffmpegPath))
{
- // 3) Search system $PATH environment variable for valid FFmpeg
- if (!ValidatePath(ExistsOnSystemPath("ffmpeg"), FFmpegLocation.System))
- {
- EncoderLocation = FFmpegLocation.NotFound;
- _ffmpegPath = null;
- }
+ // 3) Check "ffmpeg"
+ ffmpegPath = "ffmpeg";
}
}
+ if (!ValidatePath(ffmpegPath))
+ {
+ _ffmpegPath = null;
+ }
+
// Write the FFmpeg path to the config/encoding.xml file as <EncoderAppPathDisplay> so it appears in UI
var config = _configurationManager.GetEncodingOptions();
config.EncoderAppPathDisplay = _ffmpegPath ?? string.Empty;
@@ -130,11 +129,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableDecoders(validator.GetDecoders());
SetAvailableEncoders(validator.GetEncoders());
+ SetAvailableFilters(validator.GetFilters());
+ SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
SetAvailableHwaccels(validator.GetHwaccels());
- threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
+ SetMediaEncoderVersion(validator);
+
+ _threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
}
- _logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty);
+ _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
}
/// <summary>
@@ -145,6 +148,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <param name="pathType">The path type.</param>
public void UpdateEncoderPath(string path, string pathType)
{
+ var config = _configurationManager.GetEncodingOptions();
+
+ // Filesystem may not be case insensitive, but EncoderAppPathDisplay should always point to a valid file?
+ if (string.IsNullOrEmpty(config.EncoderAppPath)
+ && string.Equals(config.EncoderAppPathDisplay, path, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Existing ffmpeg path is empty and the new path is the same as {EncoderAppPathDisplay}. Skipping", nameof(config.EncoderAppPathDisplay));
+ return;
+ }
+
string newPath;
_logger.LogInformation("Attempting to update encoder path to {Path}. pathType: {PathType}", path ?? string.Empty, pathType ?? string.Empty);
@@ -153,28 +166,32 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
throw new ArgumentException("Unexpected pathType value");
}
- else if (string.IsNullOrWhiteSpace(path))
+
+ if (string.IsNullOrWhiteSpace(path))
{
// User had cleared the custom path in UI
newPath = string.Empty;
}
- else if (File.Exists(path))
- {
- newPath = path;
- }
- else if (Directory.Exists(path))
- {
- // Given path is directory, so resolve down to filename
- newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
- }
else
{
- throw new ResourceNotFoundException();
+ if (Directory.Exists(path))
+ {
+ // Given path is directory, so resolve down to filename
+ newPath = GetEncoderPathFromDirectory(path, "ffmpeg");
+ }
+ else
+ {
+ newPath = path;
+ }
+
+ if (!new EncoderValidator(_logger, newPath).ValidateVersion())
+ {
+ throw new ResourceNotFoundException();
+ }
}
// Write the new ffmpeg path to the xml as <EncoderAppPath>
// This ensures its not lost on next startup
- var config = _configurationManager.GetEncodingOptions();
config.EncoderAppPath = newPath;
_configurationManager.SaveConfiguration("encoding", config);
@@ -184,37 +201,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <summary>
/// Validates the supplied FQPN to ensure it is a ffmpeg utility.
- /// If checks pass, global variable FFmpegPath and EncoderLocation are updated.
+ /// If checks pass, global variable FFmpegPath is updated.
/// </summary>
/// <param name="path">FQPN to test.</param>
- /// <param name="location">Location (External, Custom, System) of tool.</param>
/// <returns><c>true</c> if the version validation succeeded; otherwise, <c>false</c>.</returns>
- private bool ValidatePath(string path, FFmpegLocation location)
+ private bool ValidatePath(string path)
{
- bool rc = false;
-
- if (!string.IsNullOrEmpty(path))
+ if (string.IsNullOrEmpty(path))
{
- if (File.Exists(path))
- {
- rc = new EncoderValidator(_logger, path).ValidateVersion();
-
- if (!rc)
- {
- _logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path);
- }
+ return false;
+ }
- _ffmpegPath = path;
- EncoderLocation = location;
- return true;
- }
- else
- {
- _logger.LogWarning("FFmpeg: {Location}: File not found: {Path}", location, path);
- }
+ bool rc = new EncoderValidator(_logger, path).ValidateVersion();
+ if (!rc)
+ {
+ _logger.LogWarning("FFmpeg: Failed version check: {Path}", path);
+ return false;
}
- return rc;
+ _ffmpegPath = path;
+ return true;
}
private string GetEncoderPathFromDirectory(string path, string filename, bool recursive = false)
@@ -235,34 +241,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- /// <summary>
- /// Search the system $PATH environment variable looking for given filename.
- /// </summary>
- /// <param name="fileName">The filename.</param>
- /// <returns>The full path to the file.</returns>
- private string ExistsOnSystemPath(string fileName)
- {
- var inJellyfinPath = GetEncoderPathFromDirectory(AppContext.BaseDirectory, fileName, recursive: true);
- if (!string.IsNullOrEmpty(inJellyfinPath))
- {
- return inJellyfinPath;
- }
-
- var values = Environment.GetEnvironmentVariable("PATH");
-
- foreach (var path in values.Split(Path.PathSeparator))
- {
- var candidatePath = GetEncoderPathFromDirectory(path, fileName);
-
- if (!string.IsNullOrEmpty(candidatePath))
- {
- return candidatePath;
- }
- }
-
- return null;
- }
-
public void SetAvailableEncoders(IEnumerable<string> list)
{
_encoders = list.ToList();
@@ -278,6 +256,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
_hwaccels = list.ToList();
}
+ public void SetAvailableFilters(IEnumerable<string> list)
+ {
+ _filters = list.ToList();
+ }
+
+ public void SetAvailableFiltersWithOption(IDictionary<int, bool> dict)
+ {
+ _filtersWithOption = dict;
+ }
+
+ public void SetMediaEncoderVersion(EncoderValidator validator)
+ {
+ _ffmpegVersion = validator.GetFFmpegVersion();
+ }
+
public bool SupportsEncoder(string encoder)
{
return _encoders.Contains(encoder, StringComparer.OrdinalIgnoreCase);
@@ -293,17 +286,26 @@ namespace MediaBrowser.MediaEncoding.Encoder
return _hwaccels.Contains(hwaccel, StringComparer.OrdinalIgnoreCase);
}
- public bool SupportsFilter(string filter, string option)
+ public bool SupportsFilter(string filter)
{
- if (_ffmpegPath != null)
+ return _filters.Contains(filter, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public bool SupportsFilterWithOption(FilterOptionType option)
+ {
+ if (_filtersWithOption.TryGetValue((int)option, out var val))
{
- var validator = new EncoderValidator(_logger, _ffmpegPath);
- return validator.CheckFilter(filter, option);
+ return val;
}
return false;
}
+ public Version GetMediaEncoderVersion()
+ {
+ return _ffmpegVersion;
+ }
+
public bool CanEncodeToAudioCodec(string codec)
{
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
@@ -394,7 +396,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var args = extractChapters
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
- args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim();
+ args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
var process = new Process
{
@@ -477,17 +479,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
Protocol = MediaProtocol.File
};
- return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken);
+ return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ImageFormat.Jpg, cancellationToken);
}
public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
- return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken);
+ return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, ImageFormat.Jpg, cancellationToken);
}
- public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken)
+ public Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
- return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken);
+ return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken);
}
private async Task<string> ExtractImage(
@@ -499,24 +501,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool isAudio,
Video3DFormat? threedFormat,
TimeSpan? offset,
+ ImageFormat? targetFormat,
CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
- if (isAudio)
- {
- if (imageStreamIndex.HasValue && imageStreamIndex.Value > 0)
- {
- // It seems for audio files we need to subtract 1 (for the audio stream??)
- imageStreamIndex = imageStreamIndex.Value - 1;
- }
- }
- else
+ if (!isAudio)
{
// The failure of HDR extraction usually occurs when using custom ffmpeg that does not contain the zscale filter.
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -529,7 +524,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -542,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -554,17 +549,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, targetFormat, cancellationToken).ConfigureAwait(false);
}
- private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, CancellationToken cancellationToken)
+ private async Task<string> ExtractImageInternal(string inputPath, string container, MediaStream videoStream, int? imageStreamIndex, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, bool allowTonemap, ImageFormat? targetFormat, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException(nameof(inputPath));
}
- var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
+ var outputExtension = targetFormat switch
+ {
+ ImageFormat.Bmp => ".bmp",
+ ImageFormat.Gif => ".gif",
+ ImageFormat.Jpg => ".jpg",
+ ImageFormat.Png => ".png",
+ ImageFormat.Webp => ".webp",
+ _ => ".jpg"
+ };
+
+ var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
@@ -582,7 +587,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ => string.Empty
};
- var mapArg = imageStreamIndex.HasValue ? (" -map 0:v:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
+ var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty;
var enableHdrExtraction = allowTonemap && string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase);
if (enableHdrExtraction)
@@ -615,7 +620,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
+ var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads);
if (offset.HasValue)
{
@@ -679,11 +684,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1 || !file.Exists || file.Length == 0)
{
- var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath);
+ _logger.LogError("ffmpeg image extraction failed for {Path}", inputPath);
- _logger.LogError(msg);
-
- throw new FfmpegException(msg);
+ throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath));
}
return tempExtractPath;
@@ -699,118 +702,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public string GetTimeParameter(TimeSpan time)
{
- return time.ToString(@"hh\:mm\:ss\.fff", _usCulture);
- }
-
- public async Task ExtractVideoImagesOnInterval(
- string inputFile,
- string container,
- MediaStream videoStream,
- MediaSourceInfo mediaSource,
- Video3DFormat? threedFormat,
- TimeSpan interval,
- string targetDirectory,
- string filenamePrefix,
- int? maxWidth,
- CancellationToken cancellationToken)
- {
- var inputArgument = GetInputArgument(inputFile, mediaSource);
-
- var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture);
-
- if (maxWidth.HasValue)
- {
- var maxWidthParam = maxWidth.Value.ToString(_usCulture);
-
- vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam);
- }
-
- Directory.CreateDirectory(targetDirectory);
- var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
-
- var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads);
-
- if (!string.IsNullOrWhiteSpace(container))
- {
- var inputFormat = EncodingHelper.GetInputFormat(container);
- if (!string.IsNullOrWhiteSpace(inputFormat))
- {
- args = "-f " + inputFormat + " " + args;
- }
- }
-
- var processStartInfo = new ProcessStartInfo
- {
- CreateNoWindow = true,
- UseShellExecute = false,
- FileName = _ffmpegPath,
- Arguments = args,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- };
-
- _logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments);
-
- await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- bool ranToCompletion = false;
-
- var process = new Process
- {
- StartInfo = processStartInfo,
- EnableRaisingEvents = true
- };
- using (var processWrapper = new ProcessWrapper(process, this))
- {
- try
- {
- StartProcess(processWrapper);
-
- // Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
- // but we still need to detect if the process hangs.
- // Making the assumption that as long as new jpegs are showing up, everything is good.
-
- bool isResponsive = true;
- int lastCount = 0;
-
- while (isResponsive)
- {
- if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false))
- {
- ranToCompletion = true;
- break;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- var jpegCount = _fileSystem.GetFilePaths(targetDirectory)
- .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase));
-
- isResponsive = jpegCount > lastCount;
- lastCount = jpegCount;
- }
-
- if (!ranToCompletion)
- {
- StopProcess(processWrapper, 1000);
- }
- }
- finally
- {
- _thumbnailResourcePool.Release();
- }
-
- var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
-
- if (exitCode == -1)
- {
- var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputArgument);
-
- _logger.LogError(msg);
-
- throw new FfmpegException(msg);
- }
- }
+ return time.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
}
private void StartProcess(ProcessWrapper process)
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 7733e715f..9f6d8e7fe 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,13 +6,9 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@@ -26,11 +22,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BDInfo" Version="0.7.6.1" />
- <PackageReference Include="libse" Version="3.6.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
- <PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
- <PackageReference Include="UTF.Unknown" Version="2.3.0" />
+ <PackageReference Include="BDInfo" Version="0.7.6.2" />
+ <PackageReference Include="libse" Version="3.6.4" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
+ <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
+ <PackageReference Include="UTF.Unknown" Version="2.5.0" />
</ItemGroup>
<!-- Code Analyzers-->
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 1fa90bb21..a9e753726 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -22,7 +20,7 @@ namespace MediaBrowser.MediaEncoding.Probing
throw new ArgumentNullException(nameof(result));
}
- if (result.Format != null && result.Format.Tags != null)
+ if (result.Format?.Tags != null)
{
result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags);
}
@@ -41,38 +39,16 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
- /// Gets a string from an FFProbeResult tags dictionary.
- /// </summary>
- /// <param name="tags">The tags.</param>
- /// <param name="key">The key.</param>
- /// <returns>System.String.</returns>
- public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
- {
- if (tags == null)
- {
- return null;
- }
-
- tags.TryGetValue(key, out var val);
- return val;
- }
-
- /// <summary>
/// Gets an int from an FFProbeResult tags dictionary.
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
- public static int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
+ public static int? GetDictionaryNumericValue(IReadOnlyDictionary<string, string> tags, string key)
{
- var val = GetDictionaryValue(tags, key);
-
- if (!string.IsNullOrEmpty(val))
+ if (tags.TryGetValue(key, out var val) && int.TryParse(val, out var i))
{
- if (int.TryParse(val, out var i))
- {
- return i;
- }
+ return i;
}
return null;
@@ -84,18 +60,13 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{DateTime}.</returns>
- public static DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
+ public static DateTime? GetDictionaryDateTime(IReadOnlyDictionary<string, string> tags, string key)
{
- var val = GetDictionaryValue(tags, key);
-
- if (string.IsNullOrEmpty(val))
- {
- return null;
- }
-
- if (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal, out var i))
+ if (tags.TryGetValue(key, out var val)
+ && (DateTime.TryParse(val, DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)
+ || DateTime.TryParseExact(val, "yyyy", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out dateTime)))
{
- return i.ToUniversalTime();
+ return dateTime;
}
return null;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index bbff5daca..9057a101a 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -7,7 +7,9 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -26,7 +28,8 @@ namespace MediaBrowser.MediaEncoding.Probing
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private static readonly Regex _performerPattern = new (@"(?<name>.*) \((?<instrument>.*)\)");
+
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
@@ -38,7 +41,18 @@ namespace MediaBrowser.MediaEncoding.Probing
_localization = localization;
}
- private IReadOnlyList<string> SplitWhitelist => _splitWhiteList ??= new string[] { "AC/DC" };
+ private IReadOnlyList<string> SplitWhitelist => _splitWhiteList ??= new string[]
+ {
+ "AC/DC",
+ "Au/Ra",
+ "Bremer/McCoy",
+ "이달의 소녀 1/3",
+ "LOONA 1/3",
+ "LOONA / yyxy",
+ "LOONA / ODD EYE CIRCLE",
+ "K/DA",
+ "22/7"
+ };
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
{
@@ -70,7 +84,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(data.Format.BitRate))
{
- if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(data.Format.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
info.Bitrate = value;
}
@@ -80,72 +94,38 @@ namespace MediaBrowser.MediaEncoding.Probing
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var tagStreamType = isAudio ? "audio" : "video";
- if (data.Streams != null)
- {
- var tagStream = data.Streams.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase));
+ var tagStream = data.Streams?.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase));
- if (tagStream != null && tagStream.Tags != null)
+ if (tagStream?.Tags != null)
+ {
+ foreach (var (key, value) in tagStream.Tags)
{
- foreach (var pair in tagStream.Tags)
- {
- tags[pair.Key] = pair.Value;
- }
+ tags[key] = value;
}
}
- if (data.Format != null && data.Format.Tags != null)
+ if (data.Format?.Tags != null)
{
- foreach (var pair in data.Format.Tags)
+ foreach (var (key, value) in data.Format.Tags)
{
- tags[pair.Key] = pair.Value;
+ tags[key] = value;
}
}
FetchGenres(info, tags);
- var overview = FFProbeHelpers.GetDictionaryValue(tags, "synopsis");
-
- if (string.IsNullOrWhiteSpace(overview))
- {
- overview = FFProbeHelpers.GetDictionaryValue(tags, "description");
- }
- if (string.IsNullOrWhiteSpace(overview))
- {
- overview = FFProbeHelpers.GetDictionaryValue(tags, "desc");
- }
-
- if (!string.IsNullOrWhiteSpace(overview))
- {
- info.Overview = overview;
- }
-
- var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
- if (!string.IsNullOrWhiteSpace(title))
- {
- info.Name = title;
- }
- else
- {
- title = FFProbeHelpers.GetDictionaryValue(tags, "title-eng");
- if (!string.IsNullOrWhiteSpace(title))
- {
- info.Name = title;
- }
- }
-
- var titleSort = FFProbeHelpers.GetDictionaryValue(tags, "titlesort");
- if (!string.IsNullOrWhiteSpace(titleSort))
- {
- info.ForcedSortName = titleSort;
- }
+ info.Name = tags.GetFirstNotNullNorWhiteSpaceValue("title", "title-eng");
+ info.ForcedSortName = tags.GetFirstNotNullNorWhiteSpaceValue("sort_name", "title-sort", "titlesort");
+ info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc");
info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort");
info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number");
- info.ShowName = FFProbeHelpers.GetDictionaryValue(tags, "show_name");
+ info.ShowName = tags.GetValueOrDefault("show_name");
info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
// Several different forms of retail/premiere date
info.PremiereDate =
+ FFProbeHelpers.GetDictionaryDateTime(tags, "originaldate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
@@ -153,32 +133,21 @@ namespace MediaBrowser.MediaEncoding.Probing
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
// Set common metadata for music (audio) and music videos (video)
- info.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
-
- var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
+ info.Album = tags.GetValueOrDefault("album");
- if (!string.IsNullOrWhiteSpace(artists))
+ if (tags.TryGetValue("artists", out var artists) && !string.IsNullOrWhiteSpace(artists))
{
- info.Artists = SplitArtists(artists, new[] { '/', ';' }, false)
- .DistinctNames()
- .ToArray();
+ info.Artists = SplitDistinctArtists(artists, new[] { '/', ';' }, false).ToArray();
}
else
{
- var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
- if (string.IsNullOrWhiteSpace(artist))
- {
- info.Artists = Array.Empty<string>();
- }
- else
- {
- info.Artists = SplitArtists(artist, _nameDelimiters, true)
- .DistinctNames()
- .ToArray();
- }
+ var artist = tags.GetFirstNotNullNorWhiteSpaceValue("artist");
+ info.Artists = artist == null
+ ? Array.Empty<string>()
+ : SplitDistinctArtists(artist, _nameDelimiters, true).ToArray();
}
- // If we don't have a ProductionYear try and get it from PremiereDate
+ // Guess ProductionYear from PremiereDate if missing
if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue)
{
info.ProductionYear = info.PremiereDate.Value.Year;
@@ -198,10 +167,10 @@ namespace MediaBrowser.MediaEncoding.Probing
{
FetchStudios(info, tags, "copyright");
- var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC");
- if (!string.IsNullOrWhiteSpace(iTunEXTC))
+ var iTunExtc = tags.GetFirstNotNullNorWhiteSpaceValue("iTunEXTC");
+ if (iTunExtc != null)
{
- var parts = iTunEXTC.Split('|', StringSplitOptions.RemoveEmptyEntries);
+ var parts = iTunExtc.Split('|', StringSplitOptions.RemoveEmptyEntries);
// Example
// mpaa|G|100|For crude humor
if (parts.Length > 1)
@@ -215,15 +184,15 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- var itunesXml = FFProbeHelpers.GetDictionaryValue(tags, "iTunMOVI");
- if (!string.IsNullOrWhiteSpace(itunesXml))
+ var iTunXml = tags.GetFirstNotNullNorWhiteSpaceValue("iTunMOVI");
+ if (iTunXml != null)
{
- FetchFromItunesInfo(itunesXml, info);
+ FetchFromItunesInfo(iTunXml, info);
}
if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration))
{
- info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks;
+ info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, CultureInfo.InvariantCulture)).Ticks;
}
FetchWtvInfo(info, data);
@@ -235,8 +204,7 @@ namespace MediaBrowser.MediaEncoding.Probing
ExtractTimestamp(info);
- var stereoMode = GetDictionaryValue(tags, "stereo_mode");
- if (string.Equals(stereoMode, "left_right", StringComparison.OrdinalIgnoreCase))
+ if (tags.TryGetValue("stereo_mode", out var stereoMode) && string.Equals(stereoMode, "left_right", StringComparison.OrdinalIgnoreCase))
{
info.Video3DFormat = Video3DFormat.FullSideBySide;
}
@@ -289,42 +257,36 @@ namespace MediaBrowser.MediaEncoding.Probing
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
{
- if (channelsValue <= 2)
- {
- return 192000;
- }
-
- if (channelsValue >= 5)
+ switch (channelsValue)
{
- return 320000;
+ case <= 2:
+ return 192000;
+ case >= 5:
+ return 320000;
}
}
if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
{
- if (channelsValue <= 2)
- {
- return 192000;
- }
-
- if (channelsValue >= 5)
+ switch (channelsValue)
{
- return 640000;
+ case <= 2:
+ return 192000;
+ case >= 5:
+ return 640000;
}
}
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
{
- if (channelsValue <= 2)
- {
- return 960000;
- }
-
- if (channelsValue >= 5)
+ switch (channelsValue)
{
- return 2880000;
+ case <= 2:
+ return 960000;
+ case >= 5:
+ return 2880000;
}
}
@@ -621,7 +583,8 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>MediaAttachments.</returns>
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
{
- if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase)
+ && streamInfo.Disposition?.GetValueOrDefault("attached_pic") != 1)
{
return null;
}
@@ -686,11 +649,6 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.IsAVC = false;
}
- if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase))
- {
- stream.IsInterlaced = true;
- }
-
// Filter out junk
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase))
{
@@ -712,7 +670,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(streamInfo.SampleRate))
{
- if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
stream.SampleRate = value;
}
@@ -728,6 +686,16 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.BitDepth = streamInfo.BitsPerRawSample;
}
+
+ if (string.IsNullOrEmpty(stream.Title))
+ {
+ // mp4 missing track title workaround: fall back to handler_name if populated
+ string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
+ if (!string.IsNullOrEmpty(handlerName))
+ {
+ stream.Title = handlerName;
+ }
+ }
}
else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
{
@@ -736,18 +704,43 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
stream.LocalizedForced = _localization.GetLocalizedString("Forced");
+
+ if (string.IsNullOrEmpty(stream.Title))
+ {
+ // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
+ string handlerName = GetDictionaryValue(streamInfo.Tags, "handler_name");
+ if (!string.IsNullOrEmpty(handlerName) && !string.Equals(handlerName, "SubtitleHandler", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Title = handlerName;
+ }
+ }
}
else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
{
- stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
- ? MediaStreamType.EmbeddedImage
- : MediaStreamType.Video;
-
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
- if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
+ // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe,
+ // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported
+ // frame rate (both rounded to the nearest integer) to determine if the file is interlaced
+ int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0);
+ int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0);
+
+ bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+ && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
+ bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
+ && string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+ && roundedTimeBaseFPS == roundedDoubleFrameRate;
+
+ if (videoInterlaced || h264MbaffCoded)
+ {
+ stream.IsInterlaced = true;
+ }
+
+ if (isAudio
+ || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.EmbeddedImage;
}
@@ -782,6 +775,23 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.BitDepth = streamInfo.BitsPerRawSample;
}
+ if (!stream.BitDepth.HasValue)
+ {
+ if (!string.IsNullOrEmpty(streamInfo.PixelFormat)
+ && streamInfo.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.BitDepth = 10;
+ }
+
+ if (!string.IsNullOrEmpty(streamInfo.Profile)
+ && (streamInfo.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
+ || streamInfo.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)
+ || streamInfo.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase)))
+ {
+ stream.BitDepth = 10;
+ }
+ }
+
// stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
@@ -824,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (!string.IsNullOrEmpty(streamInfo.BitRate))
{
- if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
bitrate = value;
}
@@ -837,7 +847,7 @@ namespace MediaBrowser.MediaEncoding.Probing
&& (stream.Type == MediaStreamType.Video || (isAudio && stream.Type == MediaStreamType.Audio)))
{
// If the stream info doesn't have a bitrate get the value from the media format info
- if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
+ if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
bitrate = value;
}
@@ -854,7 +864,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|| string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
{
var bps = GetBPSFromTags(streamInfo);
- if (bps != null && bps > 0)
+ if (bps > 0)
{
stream.BitRate = bps;
}
@@ -923,6 +933,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
tags.TryGetValue(key, out var val);
+
return val;
}
@@ -930,7 +941,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
if (string.IsNullOrEmpty(input))
{
- return input;
+ return null;
}
return input.Split('(').FirstOrDefault();
@@ -942,8 +953,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var parts = (original ?? string.Empty).Split(':');
if (!(parts.Length == 2 &&
- int.TryParse(parts[0], NumberStyles.Any, _usCulture, out var width) &&
- int.TryParse(parts[1], NumberStyles.Any, _usCulture, out var height) &&
+ int.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var width) &&
+ int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var height) &&
width > 0 &&
height > 0))
{
@@ -1016,66 +1027,71 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.Nullable{System.Single}.</returns>
- private float? GetFrameRate(string value)
+ internal static float? GetFrameRate(ReadOnlySpan<char> value)
{
- if (!string.IsNullOrEmpty(value))
+ if (value.IsEmpty)
{
- var parts = value.Split('/');
-
- float result;
+ return null;
+ }
- if (parts.Length == 2)
- {
- result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
- }
- else
+ int index = value.IndexOf('/');
+ if (index == -1)
+ {
+ // REVIEW: is this branch actually required? (i.e. does ffprobe ever output something other than a fraction?)
+ if (float.TryParse(value, NumberStyles.AllowThousands | NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
{
- result = float.Parse(parts[0], _usCulture);
+ return result;
}
- return float.IsNaN(result) ? (float?)null : result;
+ return null;
}
- return null;
+ if (!float.TryParse(value[..index], NumberStyles.Integer, CultureInfo.InvariantCulture, out var dividend)
+ || !float.TryParse(value[(index + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var divisor))
+ {
+ return null;
+ }
+
+ return divisor == 0f ? null : dividend / divisor;
}
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
{
- if (result.Streams != null)
+ // Get the first info stream
+ var stream = result.Streams?.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase));
+ if (stream == null)
{
- // Get the first info stream
- var stream = result.Streams.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase));
+ return;
+ }
- if (stream != null)
- {
- // Get duration from stream properties
- var duration = stream.Duration;
+ // Get duration from stream properties
+ var duration = stream.Duration;
- // If it's not there go into format properties
- if (string.IsNullOrEmpty(duration))
- {
- duration = result.Format.Duration;
- }
+ // If it's not there go into format properties
+ if (string.IsNullOrEmpty(duration))
+ {
+ duration = result.Format.Duration;
+ }
- // If we got something, parse it
- if (!string.IsNullOrEmpty(duration))
- {
- data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
- }
- }
+ // If we got something, parse it
+ if (!string.IsNullOrEmpty(duration))
+ {
+ data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, CultureInfo.InvariantCulture)).Ticks;
}
}
private int? GetBPSFromTags(MediaStreamInfo streamInfo)
{
- if (streamInfo != null && streamInfo.Tags != null)
+ if (streamInfo?.Tags == null)
{
- var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
- if (!string.IsNullOrEmpty(bps)
- && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
- {
- return parsedBps;
- }
+ return null;
+ }
+
+ var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
+ if (!string.IsNullOrEmpty(bps)
+ && int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
+ {
+ return parsedBps;
}
return null;
@@ -1083,13 +1099,15 @@ namespace MediaBrowser.MediaEncoding.Probing
private double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo)
{
- if (streamInfo != null && streamInfo.Tags != null)
+ if (streamInfo?.Tags == null)
{
- var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
- if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
- {
- return parsedDuration.TotalSeconds;
- }
+ return null;
+ }
+
+ var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
+ if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
+ {
+ return parsedDuration.TotalSeconds;
}
return null;
@@ -1097,14 +1115,17 @@ namespace MediaBrowser.MediaEncoding.Probing
private long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo)
{
- if (streamInfo != null && streamInfo.Tags != null)
+ if (streamInfo?.Tags == null)
{
- var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng") ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
- if (!string.IsNullOrEmpty(numberOfBytes)
- && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
- {
- return parsedBytes;
- }
+ return null;
+ }
+
+ var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng")
+ ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
+ if (!string.IsNullOrEmpty(numberOfBytes)
+ && long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
+ {
+ return parsedBytes;
}
return null;
@@ -1112,24 +1133,18 @@ namespace MediaBrowser.MediaEncoding.Probing
private void SetSize(InternalMediaInfoResult data, MediaInfo info)
{
- if (data.Format != null)
+ if (data.Format == null)
{
- if (!string.IsNullOrEmpty(data.Format.Size))
- {
- info.Size = long.Parse(data.Format.Size, _usCulture);
- }
- else
- {
- info.Size = null;
- }
+ return;
}
+
+ info.Size = string.IsNullOrEmpty(data.Format.Size) ? null : long.Parse(data.Format.Size, CultureInfo.InvariantCulture);
}
- private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
+ private void SetAudioInfoFromTags(MediaInfo audio, IReadOnlyDictionary<string, string> tags)
{
var people = new List<BaseItemPerson>();
- var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
- if (!string.IsNullOrWhiteSpace(composer))
+ if (tags.TryGetValue("composer", out var composer) && !string.IsNullOrWhiteSpace(composer))
{
foreach (var person in Split(composer, false))
{
@@ -1137,8 +1152,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- var conductor = FFProbeHelpers.GetDictionaryValue(tags, "conductor");
- if (!string.IsNullOrWhiteSpace(conductor))
+ if (tags.TryGetValue("conductor", out var conductor) && !string.IsNullOrWhiteSpace(conductor))
{
foreach (var person in Split(conductor, false))
{
@@ -1146,8 +1160,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
- if (!string.IsNullOrWhiteSpace(lyricist))
+ if (tags.TryGetValue("lyricist", out var lyricist) && !string.IsNullOrWhiteSpace(lyricist))
{
foreach (var person in Split(lyricist, false))
{
@@ -1155,9 +1168,27 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- // Check for writer some music is tagged that way as alternative to composer/lyricist
- var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
- if (!string.IsNullOrWhiteSpace(writer))
+ if (tags.TryGetValue("performer", out var performer) && !string.IsNullOrWhiteSpace(performer))
+ {
+ foreach (var person in Split(performer, false))
+ {
+ Match match = _performerPattern.Match(person);
+
+ // If the performer doesn't have any instrument/role associated, it won't match. In that case, chances are it's simply a band name, so we skip it.
+ if (match.Success)
+ {
+ people.Add(new BaseItemPerson
+ {
+ Name = match.Groups["name"].Value,
+ Type = PersonType.Actor,
+ Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
+ });
+ }
+ }
+ }
+
+ // In cases where there isn't sufficient information as to which role a writer performed on a recording, tagging software uses the "writer" tag.
+ if (tags.TryGetValue("writer", out var writer) && !string.IsNullOrWhiteSpace(writer))
{
foreach (var person in Split(writer, false))
{
@@ -1165,40 +1196,57 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- audio.People = people.ToArray();
-
- var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
- if (string.IsNullOrWhiteSpace(albumArtist))
+ if (tags.TryGetValue("arranger", out var arranger) && !string.IsNullOrWhiteSpace(arranger))
{
- albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
+ foreach (var person in Split(arranger, false))
+ {
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Arranger });
+ }
}
- if (string.IsNullOrWhiteSpace(albumArtist))
+ if (tags.TryGetValue("engineer", out var engineer) && !string.IsNullOrWhiteSpace(engineer))
{
- albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
+ foreach (var person in Split(engineer, false))
+ {
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Engineer });
+ }
}
- if (string.IsNullOrWhiteSpace(albumArtist))
+ if (tags.TryGetValue("mixer", out var mixer) && !string.IsNullOrWhiteSpace(mixer))
{
- audio.AlbumArtists = Array.Empty<string>();
+ foreach (var person in Split(mixer, false))
+ {
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Mixer });
+ }
}
- else
+
+ if (tags.TryGetValue("remixer", out var remixer) && !string.IsNullOrWhiteSpace(remixer))
{
- audio.AlbumArtists = SplitArtists(albumArtist, _nameDelimiters, true)
- .DistinctNames()
- .ToArray();
+ foreach (var person in Split(remixer, false))
+ {
+ people.Add(new BaseItemPerson { Name = person, Type = PersonType.Remixer });
+ }
}
+ audio.People = people.ToArray();
+
+ // Set album artist
+ var albumArtist = tags.GetFirstNotNullNorWhiteSpaceValue("albumartist", "album artist", "album_artist");
+ audio.AlbumArtists = albumArtist != null
+ ? SplitDistinctArtists(albumArtist, _nameDelimiters, true).ToArray()
+ : Array.Empty<string>();
+
+ // Set album artist to artist if empty
if (audio.AlbumArtists.Length == 0)
{
audio.AlbumArtists = audio.Artists;
}
// Track number
- audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
+ audio.IndexNumber = GetDictionaryTrackOrDiscNumber(tags, "track");
// Disc number
- audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
+ audio.ParentIndexNumber = GetDictionaryTrackOrDiscNumber(tags, "disc");
// There's several values in tags may or may not be present
FetchStudios(audio, tags, "organization");
@@ -1206,30 +1254,25 @@ namespace MediaBrowser.MediaEncoding.Probing
FetchStudios(audio, tags, "publisher");
FetchStudios(audio, tags, "label");
- // These support mulitple values, but for now we only store the first.
- var mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id"))
- ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMARTISTID"));
-
+ // These support multiple values, but for now we only store the first.
+ var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id"))
+ ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID"));
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id"))
- ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ARTISTID"));
-
+ mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id"))
+ ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID"));
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id"))
- ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_ALBUMID"));
-
+ mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id"))
+ ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID"));
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id"))
- ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASEGROUPID"));
-
+ mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id"))
+ ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID"));
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb);
- mb = GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id"))
- ?? GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MUSICBRAINZ_RELEASETRACKID"));
-
+ mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id"))
+ ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID"));
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb);
}
@@ -1253,18 +1296,18 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <returns>System.String[][].</returns>
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
{
- // Only use the comma as a delimeter if there are no slashes or pipes.
+ // Only use the comma as a delimiter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
- var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ?
+ var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ?
_nameDelimiters :
new[] { ',' };
- return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
+ return val.Split(delimiter, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim());
}
- private IEnumerable<string> SplitArtists(string val, char[] delimiters, bool splitFeaturing)
+ private IEnumerable<string> SplitDistinctArtists(string val, char[] delimiters, bool splitFeaturing)
{
if (splitFeaturing)
{
@@ -1290,7 +1333,7 @@ namespace MediaBrowser.MediaEncoding.Probing
.Select(i => i.Trim());
artistsFound.AddRange(artists);
- return artistsFound;
+ return artistsFound.DistinctNames();
}
/// <summary>
@@ -1299,36 +1342,38 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <param name="info">The info.</param>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
- private void FetchStudios(MediaInfo info, Dictionary<string, string> tags, string tagName)
+ private void FetchStudios(MediaInfo info, IReadOnlyDictionary<string, string> tags, string tagName)
{
- var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
+ var val = tags.GetValueOrDefault(tagName);
- if (!string.IsNullOrEmpty(val))
+ if (string.IsNullOrEmpty(val))
{
- var studios = Split(val, true);
- var studioList = new List<string>();
+ return;
+ }
- foreach (var studio in studios)
- {
- // Sometimes the artist name is listed here, account for that
- if (info.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
+ var studios = Split(val, true);
+ var studioList = new List<string>();
- if (info.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
+ foreach (var studio in studios)
+ {
+ if (string.IsNullOrWhiteSpace(studio))
+ {
+ continue;
+ }
- studioList.Add(studio);
+ // Don't add artist/album artist name to studios, even if it's listed there
+ if (info.Artists.Contains(studio, StringComparison.OrdinalIgnoreCase)
+ || info.AlbumArtists.Contains(studio, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
}
- info.Studios = studioList
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
+ studioList.Add(studio);
}
+
+ info.Studios = studioList
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
}
/// <summary>
@@ -1336,58 +1381,55 @@ namespace MediaBrowser.MediaEncoding.Probing
/// </summary>
/// <param name="info">The information.</param>
/// <param name="tags">The tags.</param>
- private void FetchGenres(MediaInfo info, Dictionary<string, string> tags)
+ private void FetchGenres(MediaInfo info, IReadOnlyDictionary<string, string> tags)
{
- var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
+ var genreVal = tags.GetValueOrDefault("genre");
+ if (string.IsNullOrEmpty(genreVal))
+ {
+ return;
+ }
- if (!string.IsNullOrEmpty(val))
+ var genres = new List<string>(info.Genres);
+ foreach (var genre in Split(genreVal, true))
{
- var genres = new List<string>(info.Genres);
- foreach (var genre in Split(val, true))
+ if (string.IsNullOrWhiteSpace(genre))
{
- genres.Add(genre);
+ continue;
}
- info.Genres = genres
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
+ genres.Add(genre);
}
+
+ info.Genres = genres
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
}
/// <summary>
- /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'.
+ /// Gets the track or disc number, which can be in the form of '1', or '1/3'.
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
- /// <returns>System.Nullable{System.Int32}.</returns>
- private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
+ /// <returns>The track or disc number, or null, if missing or not parseable.</returns>
+ private static int? GetDictionaryTrackOrDiscNumber(IReadOnlyDictionary<string, string> tags, string tagName)
{
- var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
+ var disc = tags.GetValueOrDefault(tagName);
- if (!string.IsNullOrEmpty(disc))
+ if (!string.IsNullOrEmpty(disc) && int.TryParse(disc.AsSpan().LeftPart('/'), out var discNum))
{
- disc = disc.Split('/')[0];
-
- if (int.TryParse(disc, out var num))
- {
- return num;
- }
+ return discNum;
}
return null;
}
- private ChapterInfo GetChapterInfo(MediaChapter chapter)
+ private static ChapterInfo GetChapterInfo(MediaChapter chapter)
{
var info = new ChapterInfo();
- if (chapter.Tags != null)
+ if (chapter.Tags != null && chapter.Tags.TryGetValue("title", out string name))
{
- if (chapter.Tags.TryGetValue("title", out string name))
- {
- info.Name = name;
- }
+ info.Name = name;
}
// Limit accuracy to milliseconds to match xml saving
@@ -1404,14 +1446,14 @@ namespace MediaBrowser.MediaEncoding.Probing
private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
{
- if (data.Format == null || data.Format.Tags == null)
+ var tags = data.Format?.Tags;
+
+ if (tags == null)
{
return;
}
- var genres = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/Genre");
-
- if (!string.IsNullOrWhiteSpace(genres))
+ if (tags.TryGetValue("WM/Genre", out var genres) && !string.IsNullOrWhiteSpace(genres))
{
var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
@@ -1425,16 +1467,12 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
- var officialRating = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/ParentalRating");
-
- if (!string.IsNullOrWhiteSpace(officialRating))
+ if (tags.TryGetValue("WM/ParentalRating", out var officialRating) && !string.IsNullOrWhiteSpace(officialRating))
{
video.OfficialRating = officialRating;
}
- var people = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaCredits");
-
- if (!string.IsNullOrEmpty(people))
+ if (tags.TryGetValue("WM/MediaCredits", out var people) && !string.IsNullOrEmpty(people))
{
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
@@ -1442,29 +1480,21 @@ namespace MediaBrowser.MediaEncoding.Probing
.ToArray();
}
- var year = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/OriginalReleaseTime");
- if (!string.IsNullOrWhiteSpace(year))
+ if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
{
- if (int.TryParse(year, NumberStyles.Integer, _usCulture, out var val))
- {
- video.ProductionYear = val;
- }
+ video.ProductionYear = parsedYear;
}
- var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaOriginalBroadcastDateTime");
- if (!string.IsNullOrWhiteSpace(premiereDateString))
+ // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
+ // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
+ if (tags.TryGetValue("WM/MediaOriginalBroadcastDateTime", out var premiereDateString) && DateTime.TryParse(year, null, DateTimeStyles.AdjustToUniversal, out var parsedDate))
{
- // Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
- // DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
- if (DateTime.TryParse(year, null, DateTimeStyles.None, out var val))
- {
- video.PremiereDate = val.ToUniversalTime();
- }
+ video.PremiereDate = parsedDate;
}
- var description = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitleDescription");
+ var description = tags.GetValueOrDefault("WM/SubTitleDescription");
- var subTitle = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitle");
+ var subTitle = tags.GetValueOrDefault("WM/SubTitle");
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
@@ -1475,49 +1505,48 @@ namespace MediaBrowser.MediaEncoding.Probing
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (string.IsNullOrWhiteSpace(subTitle)
&& !string.IsNullOrWhiteSpace(description)
- && description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
+ && description.AsSpan()[..Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)].Contains(':')) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
- string[] parts = description.Split(':');
- if (parts.Length > 0)
+ string[] descriptionParts = description.Split(':');
+ if (descriptionParts.Length > 0)
{
- string subtitle = parts[0];
+ string subtitle = descriptionParts[0];
try
{
- if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number
+ // Check if it contains a episode number and season number
+ if (subtitle.Contains('/', StringComparison.Ordinal))
{
- string[] numbers = subtitle.Split(' ');
- video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0], CultureInfo.InvariantCulture);
- int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1], CultureInfo.InvariantCulture);
+ string[] subtitleParts = subtitle.Split(' ');
+ string[] numbers = subtitleParts[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/');
+ video.IndexNumber = int.Parse(numbers[0], CultureInfo.InvariantCulture);
+ // int totalEpisodesInSeason = int.Parse(numbers[1], CultureInfo.InvariantCulture);
- description = string.Join(' ', numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
+ // Skip the numbers, concatenate the rest, trim and set as new description
+ description = string.Join(' ', subtitleParts, 1, subtitleParts.Length - 1).Trim();
+ }
+ else if (subtitle.Contains('.', StringComparison.Ordinal))
+ {
+ var subtitleParts = subtitle.Split('.');
+ description = string.Join('.', subtitleParts, 1, subtitleParts.Length - 1).Trim();
}
else
{
- // Switch to default parsing
- if (subtitle.Contains('.', StringComparison.Ordinal))
- {
- // skip the comment, keep the subtitle
- description = string.Join('.', subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
- }
- else
- {
- description = subtitle.Trim(); // Clean up whitespaces and save it
- }
+ description = subtitle.Trim();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while parsing subtitle field");
- // Default parsing
+ // Fallback to default parsing
if (subtitle.Contains('.', StringComparison.Ordinal))
{
- // skip the comment, keep the subtitle
- description = string.Join('.', subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
+ var subtitleParts = subtitle.Split('.');
+ description = string.Join('.', subtitleParts, 1, subtitleParts.Length - 1).Trim();
}
else
{
- description = subtitle.Trim(); // Clean up whitespaces and save it
+ description = subtitle.Trim();
}
}
}
@@ -1531,24 +1560,27 @@ namespace MediaBrowser.MediaEncoding.Probing
private void ExtractTimestamp(MediaInfo video)
{
- if (video.VideoType == VideoType.VideoFile)
+ if (video.VideoType != VideoType.VideoFile)
{
- if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
- {
- try
- {
- video.Timestamp = GetMpegTimestamp(video.Path);
+ return;
+ }
- _logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path);
- video.Timestamp = null;
- }
- }
+ if (!string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ try
+ {
+ video.Timestamp = GetMpegTimestamp(video.Path);
+ _logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp);
+ }
+ catch (Exception ex)
+ {
+ video.Timestamp = null;
+ _logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path);
}
}
@@ -1557,7 +1589,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
var packetBuffer = new byte[197];
- using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
{
fs.Read(packetBuffer);
}
@@ -1567,17 +1599,17 @@ namespace MediaBrowser.MediaEncoding.Probing
return TransportStreamTimestamp.None;
}
- if ((packetBuffer[4] == 71) && (packetBuffer[196] == 71))
+ if ((packetBuffer[4] != 71) || (packetBuffer[196] != 71))
{
- if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
- {
- return TransportStreamTimestamp.Zero;
- }
+ return TransportStreamTimestamp.None;
+ }
- return TransportStreamTimestamp.Valid;
+ if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
+ {
+ return TransportStreamTimestamp.Zero;
}
- return TransportStreamTimestamp.None;
+ return TransportStreamTimestamp.Valid;
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 639a34d99..52c1b6467 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -2,10 +2,10 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core;
+using Nikse.SubtitleEdit.Core.Common;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
@@ -38,7 +38,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
subRip.LoadSubtitle(subtitle, lines, "untitled");
if (subRip.ErrorCount > 0)
{
- _logger.LogError("{ErrorCount} errors encountered while parsing subtitle.");
+ _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount);
}
var trackInfo = new SubtitleTrackInfo();
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 608ebf443..89365a516 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -192,10 +193,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- return File.OpenRead(fileInfo.Path);
+ return AsyncFile.OpenRead(fileInfo.Path);
}
- private async Task<SubtitleInfo> GetReadableFile(
+ internal async Task<SubtitleInfo> GetReadableFile(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -205,9 +206,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string outputFormat;
string outputCodec;
- if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
+ 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";
@@ -238,7 +239,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
- if (TryGetReader(currentFormat, out _))
+ if (!TryGetReader(currentFormat, out _))
{
// Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
@@ -248,12 +249,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
}
- if (subtitleStream.IsExternal)
- {
- return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
- }
-
- return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true);
+ // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
+ return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
}
private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
@@ -639,17 +636,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (failed)
{
- var msg = $"ffmpeg subtitle extraction failed for {inputPath} to {outputPath}";
+ _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
- _logger.LogError(msg);
-
- throw new FfmpegException(msg);
+ throw new FfmpegException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
- var msg = $"ffmpeg subtitle extraction completed for {inputPath} to {outputPath}";
-
- _logger.LogInformation(msg);
+ _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
@@ -671,7 +665,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
string text;
Encoding encoding;
- using (var fileStream = File.OpenRead(file))
+ using (var fileStream = AsyncFile.OpenRead(file))
using (var reader = new StreamReader(fileStream, true))
{
encoding = reader.CurrentEncoding;
@@ -683,8 +677,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal))
{
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
+ using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
using (var writer = new StreamWriter(fileStream, encoding))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
@@ -750,13 +743,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
case MediaProtocol.File:
- return File.OpenRead(path);
+ return AsyncFile.OpenRead(path);
default:
throw new ArgumentOutOfRangeException(nameof(protocol));
}
}
- private struct SubtitleInfo
+ internal readonly struct SubtitleInfo
{
public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
{
@@ -766,13 +759,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IsExternal = isExternal;
}
- public string Path { get; set; }
+ public string Path { get; }
- public MediaProtocol Protocol { get; set; }
+ public MediaProtocol Protocol { get; }
- public string Format { get; set; }
+ public string Format { get; }
- public bool IsExternal { get; set; }
+ public bool IsExternal { get; }
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
index ad32cb794..38ef57dee 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
@@ -18,14 +18,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
writer.WriteLine("WEBVTT");
- writer.WriteLine(string.Empty);
- writer.WriteLine("REGION");
- writer.WriteLine("id:subtitle");
- writer.WriteLine("width:80%");
- writer.WriteLine("lines:3");
- writer.WriteLine("regionanchor:50%,100%");
- writer.WriteLine("viewportanchor:50%,90%");
- writer.WriteLine(string.Empty);
+ writer.WriteLine();
+ writer.WriteLine("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%");
+ writer.WriteLine();
foreach (var trackEvent in info.TrackEvents)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -39,7 +34,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
}
- writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle", startTime, endTime);
+ writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle line:90%", startTime, endTime);
var text = trackEvent.Text;
diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs
index 1d47ef9f6..f83dde56d 100644
--- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs
+++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs
@@ -1,14 +1,27 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Model.Activity
{
+ /// <summary>
+ /// An activity log entry.
+ /// </summary>
public class ActivityLogEntry
{
/// <summary>
+ /// Initializes a new instance of the <see cref="ActivityLogEntry"/> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="userId">The user id.</param>
+ public ActivityLogEntry(string name, string type, Guid userId)
+ {
+ Name = name;
+ Type = type;
+ UserId = userId;
+ }
+
+ /// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>The identifier.</value>
@@ -24,13 +37,13 @@ namespace MediaBrowser.Model.Activity
/// Gets or sets the overview.
/// </summary>
/// <value>The overview.</value>
- public string Overview { get; set; }
+ public string? Overview { get; set; }
/// <summary>
/// Gets or sets the short overview.
/// </summary>
/// <value>The short overview.</value>
- public string ShortOverview { get; set; }
+ public string? ShortOverview { get; set; }
/// <summary>
/// Gets or sets the type.
@@ -42,7 +55,7 @@ namespace MediaBrowser.Model.Activity
/// Gets or sets the item identifier.
/// </summary>
/// <value>The item identifier.</value>
- public string ItemId { get; set; }
+ public string? ItemId { get; set; }
/// <summary>
/// Gets or sets the date.
@@ -61,7 +74,7 @@ namespace MediaBrowser.Model.Activity
/// </summary>
/// <value>The user primary image tag.</value>
[Obsolete("UserPrimaryImageTag is not used.")]
- public string UserPrimaryImageTag { get; set; }
+ public string? UserPrimaryImageTag { get; set; }
/// <summary>
/// Gets or sets the log severity.
diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs
index 5ddf1e7e6..7f19a5b85 100644
--- a/MediaBrowser.Model/Branding/BrandingOptions.cs
+++ b/MediaBrowser.Model/Branding/BrandingOptions.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Model.Branding
@@ -9,12 +8,12 @@ namespace MediaBrowser.Model.Branding
/// Gets or sets the login disclaimer.
/// </summary>
/// <value>The login disclaimer.</value>
- public string LoginDisclaimer { get; set; }
+ public string? LoginDisclaimer { get; set; }
/// <summary>
/// Gets or sets the custom CSS.
/// </summary>
/// <value>The custom CSS.</value>
- public string CustomCss { get; set; }
+ public string? CustomCss { get; set; }
}
}
diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs
index d925b78b6..1ca8e80a6 100644
--- a/MediaBrowser.Model/Channels/ChannelFeatures.cs
+++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -7,11 +6,14 @@ namespace MediaBrowser.Model.Channels
{
public class ChannelFeatures
{
- public ChannelFeatures()
+ public ChannelFeatures(string name, Guid id)
{
MediaTypes = Array.Empty<ChannelMediaType>();
ContentTypes = Array.Empty<ChannelMediaContentType>();
DefaultSortFields = Array.Empty<ChannelItemSortField>();
+
+ Name = name;
+ Id = id;
}
/// <summary>
@@ -24,7 +26,7 @@ namespace MediaBrowser.Model.Channels
/// Gets or sets the identifier.
/// </summary>
/// <value>The identifier.</value>
- public string Id { get; set; }
+ public Guid Id { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance can search.
diff --git a/MediaBrowser.Model/Channels/ChannelInfo.cs b/MediaBrowser.Model/Channels/ChannelInfo.cs
deleted file mode 100644
index f2432aaeb..000000000
--- a/MediaBrowser.Model/Channels/ChannelInfo.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Channels
-{
- public class ChannelInfo
- {
- /// <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 home page URL.
- /// </summary>
- /// <value>The home page URL.</value>
- public string HomePageUrl { get; set; }
-
- /// <summary>
- /// Gets or sets the features.
- /// </summary>
- /// <value>The features.</value>
- public ChannelFeatures Features { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs
index 59966127f..f9380ce3a 100644
--- a/MediaBrowser.Model/Channels/ChannelQuery.cs
+++ b/MediaBrowser.Model/Channels/ChannelQuery.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -13,13 +12,13 @@ namespace MediaBrowser.Model.Channels
/// Gets or sets the fields to return within the items, in addition to basic information.
/// </summary>
/// <value>The fields.</value>
- public ItemFields[] Fields { get; set; }
+ public ItemFields[]? Fields { get; set; }
public bool? EnableImages { get; set; }
public int? ImageTypeLimit { get; set; }
- public ImageType[] EnableImageTypes { get; set; }
+ public ImageType[]? EnableImageTypes { get; set; }
/// <summary>
/// Gets or sets the user identifier.
diff --git a/MediaBrowser.Model/ClientLog/ClientLogEvent.cs b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs
new file mode 100644
index 000000000..21087b564
--- /dev/null
+++ b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs
@@ -0,0 +1,75 @@
+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/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
index b00d2fffb..57759a7d3 100644
--- a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
@@ -1,4 +1,3 @@
-#nullable disable
using System;
using System.Xml.Serialization;
@@ -35,21 +34,21 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets the cache path.
/// </summary>
/// <value>The cache path.</value>
- public string CachePath { get; set; }
+ public string? CachePath { get; set; }
/// <summary>
/// Gets or sets the last known version that was ran using the configuration.
/// </summary>
/// <value>The version from previous run.</value>
[XmlIgnore]
- public Version PreviousVersion { get; set; }
+ public Version? PreviousVersion { get; set; }
/// <summary>
/// Gets or sets the stringified PreviousVersion to be stored/loaded,
/// because System.Version itself isn't xml-serializable.
/// </summary>
/// <value>String value of PreviousVersion.</value>
- public string PreviousVersionStr
+ public string? PreviousVersionStr
{
get => PreviousVersion?.ToString();
set
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 24698360e..d3ce6aa7f 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -17,11 +16,11 @@ namespace MediaBrowser.Model.Configuration
SkipSubtitlesIfAudioTrackMatches = true;
RequirePerfectSubtitleMatch = true;
+ AutomaticallyAddToCollection = true;
EnablePhotos = true;
SaveSubtitlesWithMedia = true;
EnableRealtimeMonitor = true;
PathInfos = Array.Empty<MediaPathInfo>();
- EnableInternetProviders = true;
EnableAutomaticSeriesGrouping = true;
SeasonZeroDisplayName = "Specials";
}
@@ -38,6 +37,7 @@ namespace MediaBrowser.Model.Configuration
public bool SaveLocalMetadata { get; set; }
+ [Obsolete("Disable remote providers in TypeOptions instead")]
public bool EnableInternetProviders { get; set; }
public bool EnableAutomaticSeriesGrouping { get; set; }
@@ -52,21 +52,21 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets the preferred metadata language.
/// </summary>
/// <value>The preferred metadata language.</value>
- public string PreferredMetadataLanguage { get; set; }
+ public string? PreferredMetadataLanguage { get; set; }
/// <summary>
/// Gets or sets the metadata country code.
/// </summary>
/// <value>The metadata country code.</value>
- public string MetadataCountryCode { get; set; }
+ public string? MetadataCountryCode { get; set; }
public string SeasonZeroDisplayName { get; set; }
- public string[] MetadataSavers { get; set; }
+ public string[]? MetadataSavers { get; set; }
public string[] DisabledLocalMetadataReaders { get; set; }
- public string[] LocalMetadataReaderOrder { get; set; }
+ public string[]? LocalMetadataReaderOrder { get; set; }
public string[] DisabledSubtitleFetchers { get; set; }
@@ -76,15 +76,17 @@ namespace MediaBrowser.Model.Configuration
public bool SkipSubtitlesIfAudioTrackMatches { get; set; }
- public string[] SubtitleDownloadLanguages { get; set; }
+ public string[]? SubtitleDownloadLanguages { get; set; }
public bool RequirePerfectSubtitleMatch { get; set; }
public bool SaveSubtitlesWithMedia { get; set; }
+ public bool AutomaticallyAddToCollection { get; set; }
+
public TypeOptions[] TypeOptions { get; set; }
- public TypeOptions GetTypeOptions(string type)
+ public TypeOptions? GetTypeOptions(string type)
{
foreach (var options in TypeOptions)
{
diff --git a/MediaBrowser.Model/Configuration/MediaPathInfo.cs b/MediaBrowser.Model/Configuration/MediaPathInfo.cs
index 4f311c58f..a7bc43590 100644
--- a/MediaBrowser.Model/Configuration/MediaPathInfo.cs
+++ b/MediaBrowser.Model/Configuration/MediaPathInfo.cs
@@ -1,12 +1,22 @@
-#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Model.Configuration
{
public class MediaPathInfo
{
+ public MediaPathInfo(string path)
+ {
+ Path = path;
+ }
+
+ // Needed for xml serialization
+ public MediaPathInfo()
+ {
+ Path = string.Empty;
+ }
+
public string Path { get; set; }
- public string NetworkPath { get; set; }
+ public string? NetworkPath { get; set; }
}
}
diff --git a/MediaBrowser.Model/Configuration/MetadataOptions.cs b/MediaBrowser.Model/Configuration/MetadataOptions.cs
index 76b72bd08..384a7997f 100644
--- a/MediaBrowser.Model/Configuration/MetadataOptions.cs
+++ b/MediaBrowser.Model/Configuration/MetadataOptions.cs
@@ -1,5 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, CA1819
using System;
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index d1e999666..46e61ee1a 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -14,18 +14,6 @@ namespace MediaBrowser.Model.Configuration
public class ServerConfiguration : BaseApplicationConfiguration
{
/// <summary>
- /// The default value for <see cref="HttpServerPortNumber"/>.
- /// </summary>
- public const int DefaultHttpPort = 8096;
-
- /// <summary>
- /// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>.
- /// </summary>
- public const int DefaultHttpsPort = 8920;
-
- private string _baseUrl = string.Empty;
-
- /// <summary>
/// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
/// </summary>
public ServerConfiguration()
@@ -76,149 +64,13 @@ namespace MediaBrowser.Model.Configuration
}
/// <summary>
- /// Gets or sets a value indicating whether to enable automatic port forwarding.
- /// </summary>
- public bool EnableUPnP { get; set; } = false;
-
- /// <summary>
/// Gets or sets a value indicating whether to enable prometheus metrics exporting.
/// </summary>
public bool EnableMetrics { get; set; } = false;
- /// <summary>
- /// Gets or sets the public mapped port.
- /// </summary>
- /// <value>The public mapped port.</value>
- public int PublicPort { get; set; } = DefaultHttpPort;
-
- /// <summary>
- /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding.
- /// </summary>
- public bool UPnPCreateHttpPortMap { get; set; } = false;
-
- /// <summary>
- /// Gets or sets client udp port range.
- /// </summary>
- public string UDPPortRange { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets a value indicating whether IPV6 capability is enabled.
- /// </summary>
- public bool EnableIPV6 { get; set; } = false;
-
- /// <summary>
- /// Gets or sets a value indicating whether IPV4 capability is enabled.
- /// </summary>
- public bool EnableIPV4 { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log.
- /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work.
- /// </summary>
- public bool EnableSSDPTracing { get; set; } = false;
-
- /// <summary>
- /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log.
- /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
- /// </summary>
- public string SSDPTracingFilter { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the number of times SSDP UDP messages are sent.
- /// </summary>
- public int UDPSendCount { get; set; } = 2;
-
- /// <summary>
- /// Gets or sets the delay between each groups of SSDP messages (in ms).
- /// </summary>
- public int UDPSendDelay { get; set; } = 100;
-
- /// <summary>
- /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding.
- /// </summary>
- public bool IgnoreVirtualInterfaces { get; set; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>.
- /// </summary>
- public string VirtualInterfaceNames { get; set; } = "vEthernet*";
-
- /// <summary>
- /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.
- /// </summary>
- public int GatewayMonitorPeriod { get; set; } = 60;
-
- /// <summary>
- /// Gets a value indicating whether multi-socket binding is available.
- /// </summary>
- public bool EnableMultiSocketBinding { get; } = true;
-
- /// <summary>
- /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network.
- /// Depending on the address range implemented ULA ranges might not be used.
- /// </summary>
- public bool TrustAllIP6Interfaces { get; set; } = false;
-
- /// <summary>
- /// Gets or sets the ports that HDHomerun uses.
- /// </summary>
- public string HDHomerunPortRange { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets PublishedServerUri to advertise for specific subnets.
- /// </summary>
- public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets a value indicating whether Autodiscovery tracing is enabled.
- /// </summary>
- public bool AutoDiscoveryTracing { get; set; } = false;
-
- /// <summary>
- /// Gets or sets a value indicating whether Autodiscovery is enabled.
- /// </summary>
- public bool AutoDiscovery { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the public HTTPS port.
- /// </summary>
- /// <value>The public HTTPS port.</value>
- public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
-
- /// <summary>
- /// Gets or sets the HTTP server port number.
- /// </summary>
- /// <value>The HTTP server port number.</value>
- public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
-
- /// <summary>
- /// Gets or sets the HTTPS server port number.
- /// </summary>
- /// <value>The HTTPS server port number.</value>
- public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
-
- /// <summary>
- /// Gets or sets a value indicating whether to use HTTPS.
- /// </summary>
- /// <remarks>
- /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
- /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
- /// </remarks>
- public bool EnableHttps { get; set; } = false;
-
public bool EnableNormalizedItemByNameIds { get; set; } = true;
/// <summary>
- /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
- /// </summary>
- public string CertificatePath { get; set; } = string.Empty;
-
- /// <summary>
- /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
- /// </summary>
- public string CertificatePassword { get; set; } = string.Empty;
-
- /// <summary>
/// Gets or sets a value indicating whether this instance is port authorized.
/// </summary>
/// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
@@ -230,11 +82,6 @@ namespace MediaBrowser.Model.Configuration
public bool QuickConnectAvailable { get; set; } = false;
/// <summary>
- /// Gets or sets a value indicating whether access outside of the LAN is permitted.
- /// </summary>
- public bool EnableRemoteAccess { get; set; } = true;
-
- /// <summary>
/// Gets or sets a value indicating whether [enable case sensitive item ids].
/// </summary>
/// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
@@ -319,13 +166,6 @@ namespace MediaBrowser.Model.Configuration
public int LibraryMonitorDelay { get; set; } = 60;
/// <summary>
- /// Gets or sets a value indicating whether [enable dashboard response caching].
- /// Allows potential contributors without visual studio to modify production dashboard code and test changes.
- /// </summary>
- /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
- public bool EnableDashboardResponseCaching { get; set; } = true;
-
- /// <summary>
/// Gets or sets the image saving convention.
/// </summary>
/// <value>The image saving convention.</value>
@@ -337,36 +177,6 @@ namespace MediaBrowser.Model.Configuration
public string ServerName { get; set; } = string.Empty;
- public string BaseUrl
- {
- get => _baseUrl;
- set
- {
- // Normalize the start of the string
- if (string.IsNullOrWhiteSpace(value))
- {
- // If baseUrl is empty, set an empty prefix string
- _baseUrl = string.Empty;
- return;
- }
-
- if (value[0] != '/')
- {
- // If baseUrl was not configured with a leading slash, append one for consistency
- value = "/" + value;
- }
-
- // Normalize the end of the string
- if (value[value.Length - 1] == '/')
- {
- // If baseUrl was configured with a trailing slash, remove it for consistency
- value = value.Remove(value.Length - 1);
- }
-
- _baseUrl = value;
- }
- }
-
public string UICulture { get; set; } = "en-US";
public bool SaveMetadataHidden { get; set; } = false;
@@ -381,45 +191,16 @@ namespace MediaBrowser.Model.Configuration
public bool DisplaySpecialsWithinSeasons { get; set; } = true;
- /// <summary>
- /// Gets or sets the subnets that are deemed to make up the LAN.
- /// </summary>
- public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
- /// </summary>
- public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
-
public string[] CodecsUsed { get; set; } = Array.Empty<string>();
public List<RepositoryInfo> PluginRepositories { get; set; } = new List<RepositoryInfo>();
public bool EnableExternalContentInSuggestions { get; set; } = true;
- /// <summary>
- /// Gets or sets a value indicating whether the server should force connections over HTTPS.
- /// </summary>
- public bool RequireHttps { get; set; } = false;
-
- public bool EnableNewOmdbSupport { get; set; } = true;
-
- /// <summary>
- /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
- /// </summary>
- public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
-
- /// <summary>
- /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
- /// </summary>
- public bool IsRemoteIPFilterBlacklist { get; set; } = false;
-
public int ImageExtractionTimeoutMs { get; set; } = 0;
public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
- public string[] UninstalledPlugins { get; set; } = Array.Empty<string>();
-
/// <summary>
/// Gets or sets a value indicating whether slow server responses should be logged as a warning.
/// </summary>
@@ -436,11 +217,6 @@ namespace MediaBrowser.Model.Configuration
public string[] CorsHosts { get; set; } = new[] { "*" };
/// <summary>
- /// Gets or sets the known proxies.
- /// </summary>
- public string[] KnownProxies { get; set; } = Array.Empty<string>();
-
- /// <summary>
/// Gets or sets the number of days we should retain activity logs.
/// </summary>
public int? ActivityLogRetentionDays { get; set; } = 30;
@@ -459,5 +235,10 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
/// </summary>
public bool RemoveOldPlugins { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether clients should be allowed to upload logs.
+ /// </summary>
+ public bool AllowClientLogUpload { get; set; } = true;
}
}
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index 935e6cbe1..81359462c 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
@@ -33,7 +32,7 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets the audio language preference.
/// </summary>
/// <value>The audio language preference.</value>
- public string AudioLanguagePreference { get; set; }
+ public string? AudioLanguagePreference { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [play default audio track].
@@ -45,7 +44,7 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets the subtitle language preference.
/// </summary>
/// <value>The subtitle language preference.</value>
- public string SubtitleLanguagePreference { get; set; }
+ public string? SubtitleLanguagePreference { get; set; }
public bool DisplayMissingEpisodes { get; set; }
diff --git a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
index 8ad070dcb..07129d715 100644
--- a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
+++ b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Model.Configuration
@@ -13,7 +12,7 @@ namespace MediaBrowser.Model.Configuration
EnablePathSubstitution = true;
}
- public string UserId { get; set; }
+ public string? UserId { get; set; }
public string ReleaseDateFormat { get; set; }
diff --git a/MediaBrowser.Common/Cryptography/Constants.cs b/MediaBrowser.Model/Cryptography/Constants.cs
index 354114232..f2ebb5d3d 100644
--- a/MediaBrowser.Common/Cryptography/Constants.cs
+++ b/MediaBrowser.Model/Cryptography/Constants.cs
@@ -1,4 +1,4 @@
-namespace MediaBrowser.Common.Cryptography
+namespace MediaBrowser.Model.Cryptography
{
/// <summary>
/// Class containing global constants for Jellyfin Cryptography.
@@ -8,11 +8,16 @@ namespace MediaBrowser.Common.Cryptography
/// <summary>
/// The default length for new salts.
/// </summary>
- public const int DefaultSaltLength = 64;
+ public const int DefaultSaltLength = 128 / 8;
+
+ /// <summary>
+ /// The default output length.
+ /// </summary>
+ public const int DefaultOutputLength = 512 / 8;
/// <summary>
/// The default amount of iterations for hashing passwords.
/// </summary>
- public const int DefaultIterations = 1000;
+ public const int DefaultIterations = 120000;
}
}
diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
index d8b7d848a..6c521578c 100644
--- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
+++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
@@ -1,6 +1,6 @@
#pragma warning disable CS1591
-using System.Collections.Generic;
+using System;
namespace MediaBrowser.Model.Cryptography
{
@@ -8,11 +8,14 @@ namespace MediaBrowser.Model.Cryptography
{
string DefaultHashMethod { get; }
- IEnumerable<string> GetSupportedHashMethods();
+ /// <summary>
+ /// Creates a new <see cref="PasswordHash" /> instance.
+ /// </summary>
+ /// <param name="password">The password that will be hashed.</param>
+ /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns>
+ PasswordHash CreatePasswordHash(ReadOnlySpan<char> password);
- byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt);
-
- byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
+ bool Verify(PasswordHash hash, ReadOnlySpan<char> password);
byte[] GenerateSalt();
diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs
index 0e2065302..eec541041 100644
--- a/MediaBrowser.Common/Cryptography/PasswordHash.cs
+++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs
@@ -4,7 +4,7 @@ using System;
using System.Collections.Generic;
using System.Text;
-namespace MediaBrowser.Common.Cryptography
+namespace MediaBrowser.Model.Cryptography
{
// Defined from this hash storage spec
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs
index 0cccf931c..7a1c7a738 100644
--- a/MediaBrowser.Model/Devices/DeviceInfo.cs
+++ b/MediaBrowser.Model/Devices/DeviceInfo.cs
@@ -16,6 +16,11 @@ namespace MediaBrowser.Model.Devices
public string Name { get; set; }
/// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string AccessToken { get; set; }
+
+ /// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>The identifier.</value>
diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs
deleted file mode 100644
index 037ffeb5e..000000000
--- a/MediaBrowser.Model/Devices/DeviceOptions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Devices
-{
- public class DeviceOptions
- {
- public string? CustomName { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Devices/DeviceQuery.cs b/MediaBrowser.Model/Devices/DeviceQuery.cs
deleted file mode 100644
index 64e366a56..000000000
--- a/MediaBrowser.Model/Devices/DeviceQuery.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Model.Devices
-{
- public class DeviceQuery
- {
- /// <summary>
- /// Gets or sets a value indicating whether [supports synchronize].
- /// </summary>
- /// <value><c>null</c> if [supports synchronize] contains no value, <c>true</c> if [supports synchronize]; otherwise, <c>false</c>.</value>
- public bool? SupportsSync { get; set; }
-
- /// <summary>
- /// Gets or sets the user identifier.
- /// </summary>
- /// <value>The user identifier.</value>
- public Guid UserId { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs
index 8343cf028..f857bf3a8 100644
--- a/MediaBrowser.Model/Dlna/CodecProfile.cs
+++ b/MediaBrowser.Model/Dlna/CodecProfile.cs
@@ -2,8 +2,8 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using System.Xml.Serialization;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Dlna
{
@@ -58,7 +58,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var val in codec)
{
- if (codecs.Contains(val, StringComparer.OrdinalIgnoreCase))
+ if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase))
{
return true;
}
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 55c4dd074..8d03b4c0b 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -2,7 +2,7 @@
using System;
using System.Globalization;
-using System.Linq;
+using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
@@ -167,7 +167,7 @@ namespace MediaBrowser.Model.Dlna
switch (condition.Condition)
{
case ProfileConditionType.EqualsAny:
- return expected.Split('|').Contains(currentValue, StringComparer.OrdinalIgnoreCase);
+ return expected.Split('|').Contains(currentValue, StringComparison.OrdinalIgnoreCase);
case ProfileConditionType.Equals:
return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase);
case ProfileConditionType.NotEquals:
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
index 740966088..c6befdd85 100644
--- a/MediaBrowser.Model/Dlna/ContainerProfile.cs
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -1,8 +1,8 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using System.Xml.Serialization;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Dlna
{
@@ -62,7 +62,7 @@ namespace MediaBrowser.Model.Dlna
foreach (var container in allInputContainers)
{
- if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
+ if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase))
{
return !isNegativeList;
}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 600a44157..58b06ca1d 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -151,10 +151,12 @@ namespace MediaBrowser.Model.Dlna
DlnaFlags.InteractiveTransferMode |
DlnaFlags.DlnaV15;
- // if (isDirectStream)
- // {
- // flagValue = flagValue | DlnaFlags.ByteBasedSeek;
- // }
+ if (isDirectStream)
+ {
+ flagValue |= DlnaFlags.ByteBasedSeek;
+ }
+
+ // Time based seek is curently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths.
// else if (runtimeTicks.HasValue)
// {
// flagValue = flagValue | DlnaFlags.TimeBasedSeek;
@@ -208,7 +210,11 @@ namespace MediaBrowser.Model.Dlna
if (string.IsNullOrEmpty(orgPn))
{
contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
- continue;
+ }
+ else if (isDirectStream)
+ {
+ // orgOp should be added all the time once the time based seek is resolved for transcoded streams
+ contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags);
}
else
{
diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
index c511801f4..6625b7981 100644
--- a/MediaBrowser.Model/Dlna/DeviceIdentification.cs
+++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index feb3d880e..6170ff5bd 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -1,8 +1,8 @@
#pragma warning disable CA1819 // Properties should not return arrays
using System;
using System.ComponentModel;
-using System.Linq;
using System.Xml.Serialization;
+using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
@@ -253,7 +253,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -287,7 +287,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -328,7 +328,7 @@ namespace MediaBrowser.Model.Dlna
}
var audioCodecs = i.GetAudioCodecs();
- if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -469,13 +469,13 @@ namespace MediaBrowser.Model.Dlna
}
var audioCodecs = i.GetAudioCodecs();
- if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ 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, StringComparer.OrdinalIgnoreCase))
+ if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index fa3ad098f..03c3a7265 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs
index 95cd0ac27..4613bc542 100644
--- a/MediaBrowser.Model/Dlna/DlnaMaps.cs
+++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs
@@ -6,20 +6,6 @@ namespace MediaBrowser.Model.Dlna
{
public static class DlnaMaps
{
- private static readonly string DefaultStreaming =
- FlagsToString(DlnaFlags.StreamingTransferMode |
- DlnaFlags.BackgroundTransferMode |
- DlnaFlags.ConnectionStall |
- DlnaFlags.ByteBasedSeek |
- DlnaFlags.DlnaV15);
-
- private static readonly string DefaultInteractive =
- FlagsToString(DlnaFlags.InteractiveTransferMode |
- DlnaFlags.BackgroundTransferMode |
- DlnaFlags.ConnectionStall |
- DlnaFlags.ByteBasedSeek |
- DlnaFlags.DlnaV15);
-
public static string FlagsToString(DlnaFlags flags)
{
return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0);
diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
index d9bd094d9..a70ce44cc 100644
--- a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
+++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
@@ -1,4 +1,3 @@
-#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
index 20e05b8a9..06f6660f4 100644
--- a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591, CA1707
namespace MediaBrowser.Model.Dlna
{
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
index 7ce248509..93a9ae615 100644
--- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Model.Dlna
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
index 65fccbdd4..94071b419 100644
--- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -5,7 +5,7 @@ using System;
namespace MediaBrowser.Model.Dlna
{
- public class ResolutionNormalizer
+ public static class ResolutionNormalizer
{
private static readonly ResolutionConfiguration[] Configurations =
new[]
@@ -21,11 +21,7 @@ namespace MediaBrowser.Model.Dlna
public static ResolutionOptions Normalize(
int? inputBitrate,
- int? unused1,
- int? unused2,
int outputBitrate,
- string inputCodec,
- string outputCodec,
int? maxWidth,
int? maxHeight)
{
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
index 7769d0bd3..7fef16e53 100644
--- a/MediaBrowser.Model/Dlna/SortCriteria.cs
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -1,15 +1,24 @@
#pragma warning disable CS1591
+using System;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Dlna
{
public class SortCriteria
{
- public SortCriteria(string value)
+ public SortCriteria(string sortOrder)
{
+ if (!string.IsNullOrEmpty(sortOrder) && Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
+ {
+ SortOrder = sortOrderValue;
+ }
+ else
+ {
+ SortOrder = SortOrder.Ascending;
+ }
}
- public SortOrder SortOrder => SortOrder.Ascending;
+ public SortOrder SortOrder { get; }
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index f4c69fe8f..c6ce45788 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -455,7 +455,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlayProfile == null)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
@@ -677,12 +677,12 @@ namespace MediaBrowser.Model.Dlna
var videoStream = item.VideoStream;
// TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough
- var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay);
- var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream);
+ var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay);
+ var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream);
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1);
- _logger.LogInformation(
+ _logger.LogDebug(
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
@@ -694,7 +694,7 @@ namespace MediaBrowser.Model.Dlna
if (isEligibleForDirectPlay || isEligibleForDirectStream)
{
// See if it can be direct played
- var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream);
+ var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream);
var directPlay = directPlayInfo.Item1;
if (directPlay != null)
@@ -810,7 +810,7 @@ namespace MediaBrowser.Model.Dlna
// Honor requested max channels
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
- int audioBitrate = GetAudioBitrate(playlistItem.SubProtocol, options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
+ int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
isFirstAppliedCodecProfile = true;
@@ -907,7 +907,7 @@ namespace MediaBrowser.Model.Dlna
return 192000;
}
- private static int GetAudioBitrate(string subProtocol, long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item)
+ private static int GetAudioBitrate(long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item)
{
string targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
@@ -1005,7 +1005,6 @@ namespace MediaBrowser.Model.Dlna
MediaSourceInfo mediaSource,
MediaStream videoStream,
MediaStream audioStream,
- bool isEligibleForDirectPlay,
bool isEligibleForDirectStream)
{
if (options.ForceDirectPlay)
@@ -1034,7 +1033,7 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == null)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}",
container,
videoStream?.Codec ?? "no video",
@@ -1146,7 +1145,7 @@ namespace MediaBrowser.Model.Dlna
{
string audioCodec = audioStream.Codec;
conditions = new List<ProfileCondition>();
- bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
+ bool? isSecondaryAudio = mediaSource.IsSecondaryAudio(audioStream);
foreach (var i in profile.CodecProfiles)
{
@@ -1199,7 +1198,7 @@ namespace MediaBrowser.Model.Dlna
private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
type,
profile.Name ?? "Unknown Profile",
@@ -1214,6 +1213,7 @@ namespace MediaBrowser.Model.Dlna
MediaSourceInfo item,
long maxBitrate,
MediaStream subtitleStream,
+ MediaStream audioStream,
VideoOptions options,
PlayMethod playMethod)
{
@@ -1221,16 +1221,27 @@ namespace MediaBrowser.Model.Dlna
{
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null);
- if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
+ if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
+ && subtitleProfile.Method != SubtitleDeliveryMethod.External
+ && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
{
- _logger.LogInformation("Not eligible for {0} due to unsupported subtitles", playMethod);
+ _logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod);
return (false, TranscodeReason.SubtitleCodecNotSupported);
}
}
bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
+ if (!result)
+ {
+ return (false, TranscodeReason.ContainerBitrateExceedsLimit);
+ }
+
+ if (audioStream.IsExternal)
+ {
+ return (false, TranscodeReason.AudioIsExternal);
+ }
- return (result, result ? (TranscodeReason?)null : TranscodeReason.ContainerBitrateExceedsLimit);
+ return (true, null);
}
public static SubtitleProfile GetSubtitleProfile(
@@ -1262,7 +1273,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer))
+ if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
{
continue;
}
@@ -1291,7 +1302,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer))
+ if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(outputContainer))
{
continue;
}
@@ -1313,7 +1324,7 @@ namespace MediaBrowser.Model.Dlna
};
}
- private static bool IsSubtitleEmbedSupported(MediaStream subtitleStream, SubtitleProfile subtitleProfile, string transcodingSubProtocol, string transcodingContainer)
+ private static bool IsSubtitleEmbedSupported(string transcodingContainer)
{
if (!string.IsNullOrEmpty(transcodingContainer))
{
@@ -1405,7 +1416,7 @@ namespace MediaBrowser.Model.Dlna
if (itemBitrate > requestedMaxBitrate)
{
- _logger.LogInformation(
+ _logger.LogDebug(
"Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
playMethod,
itemBitrate,
@@ -1728,18 +1739,14 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!string.IsNullOrEmpty(value))
- {
- // change from split by | to comma
-
- // strip spaces to avoid having to encode
- var values = value
- .Split('|', StringSplitOptions.RemoveEmptyEntries);
+ // change from split by | to comma
+ // strip spaces to avoid having to encode
+ var values = value
+ .Split('|', StringSplitOptions.RemoveEmptyEntries);
- if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
- {
- item.SetOption(qualifier, "profile", string.Join(',', values));
- }
+ if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
+ {
+ item.SetOption(qualifier, "profile", string.Join(',', values));
}
break;
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 252872847..cf8465067 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -133,7 +133,7 @@ namespace MediaBrowser.Model.Dlna
var stream = TargetAudioStream;
return AudioSampleRate.HasValue && !IsDirectStream
? AudioSampleRate
- : stream == null ? null : stream.SampleRate;
+ : stream?.SampleRate;
}
}
@@ -146,7 +146,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
+ return TargetAudioStream?.BitDepth;
}
var targetAudioCodecs = TargetAudioCodec;
@@ -156,7 +156,7 @@ namespace MediaBrowser.Model.Dlna
return GetTargetAudioBitDepth(audioCodec);
}
- return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
+ return TargetAudioStream?.BitDepth;
}
}
@@ -169,7 +169,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+ return TargetVideoStream?.BitDepth;
}
var targetVideoCodecs = TargetVideoCodec;
@@ -179,7 +179,7 @@ namespace MediaBrowser.Model.Dlna
return GetTargetVideoBitDepth(videoCodec);
}
- return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+ return TargetVideoStream?.BitDepth;
}
}
@@ -193,7 +193,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
+ return TargetVideoStream?.RefFrames;
}
var targetVideoCodecs = TargetVideoCodec;
@@ -203,7 +203,7 @@ namespace MediaBrowser.Model.Dlna
return GetTargetRefFrames(videoCodec);
}
- return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
+ return TargetVideoStream?.RefFrames;
}
}
@@ -230,7 +230,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
+ return TargetVideoStream?.Level;
}
var targetVideoCodecs = TargetVideoCodec;
@@ -240,7 +240,7 @@ namespace MediaBrowser.Model.Dlna
return GetTargetVideoLevel(videoCodec);
}
- return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
+ return TargetVideoStream?.Level;
}
}
@@ -254,7 +254,7 @@ namespace MediaBrowser.Model.Dlna
var stream = TargetVideoStream;
return !IsDirectStream
? null
- : stream == null ? null : stream.PacketLength;
+ : stream?.PacketLength;
}
}
@@ -267,7 +267,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? null : TargetVideoStream.Profile;
+ return TargetVideoStream?.Profile;
}
var targetVideoCodecs = TargetVideoCodec;
@@ -277,7 +277,7 @@ namespace MediaBrowser.Model.Dlna
return GetOption(videoCodec, "profile");
}
- return TargetVideoStream == null ? null : TargetVideoStream.Profile;
+ return TargetVideoStream?.Profile;
}
}
@@ -292,7 +292,7 @@ namespace MediaBrowser.Model.Dlna
var stream = TargetVideoStream;
return !IsDirectStream
? null
- : stream == null ? null : stream.CodecTag;
+ : stream?.CodecTag;
}
}
@@ -306,7 +306,7 @@ namespace MediaBrowser.Model.Dlna
var stream = TargetAudioStream;
return AudioBitrate.HasValue && !IsDirectStream
? AudioBitrate
- : stream == null ? null : stream.BitRate;
+ : stream?.BitRate;
}
}
@@ -319,7 +319,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+ return TargetAudioStream?.Channels;
}
var targetAudioCodecs = TargetAudioCodec;
@@ -329,7 +329,7 @@ namespace MediaBrowser.Model.Dlna
return GetTargetRefFrames(codec);
}
- return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+ return TargetAudioStream?.Channels;
}
}
@@ -425,7 +425,7 @@ namespace MediaBrowser.Model.Dlna
return VideoBitrate.HasValue && !IsDirectStream
? VideoBitrate
- : stream == null ? null : stream.BitRate;
+ : stream?.BitRate;
}
}
@@ -451,7 +451,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic;
+ return TargetVideoStream?.IsAnamorphic;
}
return false;
@@ -464,7 +464,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
+ return TargetVideoStream?.IsInterlaced;
}
var targetVideoCodecs = TargetVideoCodec;
@@ -477,7 +477,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
+ return TargetVideoStream?.IsInterlaced;
}
}
@@ -487,7 +487,7 @@ namespace MediaBrowser.Model.Dlna
{
if (IsDirectStream)
{
- return TargetVideoStream == null ? null : TargetVideoStream.IsAVC;
+ return TargetVideoStream?.IsAVC;
}
return true;
@@ -618,30 +618,30 @@ namespace MediaBrowser.Model.Dlna
}
// Try to keep the url clean by omitting defaults
- if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
{
continue;
}
- if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
{
continue;
}
- if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
{
continue;
}
- var encodedValue = pair.Value.Replace(" ", "%20");
+ var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
}
- string queryString = string.Join("&", list.ToArray());
+ string queryString = string.Join('&', list);
return GetUrl(baseUrl, queryString);
}
@@ -681,11 +681,11 @@ namespace MediaBrowser.Model.Dlna
string audioCodecs = item.AudioCodecs.Length == 0 ?
string.Empty :
- string.Join(",", item.AudioCodecs);
+ string.Join(',', item.AudioCodecs);
string videoCodecs = item.VideoCodecs.Length == 0 ?
string.Empty :
- string.Join(",", item.VideoCodecs);
+ string.Join(',', item.VideoCodecs);
list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
@@ -794,7 +794,7 @@ namespace MediaBrowser.Model.Dlna
}
// strip spaces to avoid having to encode h264 profile names
- list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty)));
+ list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
}
if (!item.IsDirectStream)
@@ -1024,30 +1024,5 @@ namespace MediaBrowser.Model.Dlna
return count;
}
-
- public List<MediaStream> GetSelectableAudioStreams()
- {
- return GetSelectableStreams(MediaStreamType.Audio);
- }
-
- public List<MediaStream> GetSelectableSubtitleStreams()
- {
- return GetSelectableStreams(MediaStreamType.Subtitle);
- }
-
- public List<MediaStream> GetSelectableStreams(MediaStreamType type)
- {
- var list = new List<MediaStream>();
-
- foreach (var stream in MediaSource.MediaStreams)
- {
- if (type == stream.Type)
- {
- list.Add(stream);
- }
- }
-
- return list;
- }
}
}
diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
index 9b39f9e11..69bda2d91 100644
--- a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
+++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
@@ -25,6 +25,11 @@ namespace MediaBrowser.Model.Dlna
/// <summary>
/// Serve the subtitles as a separate HLS stream.
/// </summary>
- Hls = 3
+ Hls = 3,
+
+ /// <summary>
+ /// Drop the subtitle.
+ /// </summary>
+ Drop = 4
}
}
diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
index 01e3c696b..9ebde25ff 100644
--- a/MediaBrowser.Model/Dlna/SubtitleProfile.cs
+++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
@@ -2,8 +2,8 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using System.Xml.Serialization;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Dlna
{
@@ -42,7 +42,7 @@ namespace MediaBrowser.Model.Dlna
}
var languages = GetLanguages();
- return languages.Length == 0 || languages.Contains(subLanguage, StringComparer.OrdinalIgnoreCase);
+ return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 214578a85..709bdad31 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
using System.Xml.Serialization;
namespace MediaBrowser.Model.Dlna
diff --git a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs
index 249d828d3..90163ae91 100644
--- a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs
+++ b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs
@@ -1,7 +1,7 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using Jellyfin.Data.Enums;
-namespace Jellyfin.Api.Models.DisplayPreferencesDtos
+namespace MediaBrowser.Model.Dto
{
/// <summary>
/// Defines the display preferences for any item that supports them (usually Folders).
@@ -17,7 +17,7 @@ namespace Jellyfin.Api.Models.DisplayPreferencesDtos
PrimaryImageHeight = 250;
PrimaryImageWidth = 250;
ShowBackdrop = true;
- CustomPrefs = new Dictionary<string, string>();
+ CustomPrefs = new Dictionary<string, string?>();
}
/// <summary>
@@ -63,10 +63,10 @@ namespace Jellyfin.Api.Models.DisplayPreferencesDtos
public int PrimaryImageWidth { get; set; }
/// <summary>
- /// Gets the custom prefs.
+ /// Gets or sets the custom prefs.
/// </summary>
/// <value>The custom prefs.</value>
- public Dictionary<string, string> CustomPrefs { get; }
+ public Dictionary<string, string?> CustomPrefs { get; set; }
/// <summary>
/// Gets or sets the scroll direction.
diff --git a/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs b/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
deleted file mode 100644
index 1f7fe3030..000000000
--- a/MediaBrowser.Model/Entities/DisplayPreferencesDto.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-#nullable disable
-using System.Collections.Generic;
-using Jellyfin.Data.Enums;
-
-namespace MediaBrowser.Model.Entities
-{
- /// <summary>
- /// Defines the display preferences for any item that supports them (usually Folders).
- /// </summary>
- public class DisplayPreferencesDto
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
- /// </summary>
- public DisplayPreferencesDto()
- {
- RememberIndexing = false;
- PrimaryImageHeight = 250;
- PrimaryImageWidth = 250;
- ShowBackdrop = true;
- CustomPrefs = new Dictionary<string, string>();
- }
-
- /// <summary>
- /// Gets or sets the user id.
- /// </summary>
- /// <value>The user id.</value>
- public string Id { get; set; }
-
- /// <summary>
- /// Gets or sets the type of the view.
- /// </summary>
- /// <value>The type of the view.</value>
- public string ViewType { get; set; }
-
- /// <summary>
- /// Gets or sets the sort by.
- /// </summary>
- /// <value>The sort by.</value>
- public string SortBy { get; set; }
-
- /// <summary>
- /// Gets or sets the index by.
- /// </summary>
- /// <value>The index by.</value>
- public string IndexBy { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [remember indexing].
- /// </summary>
- /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value>
- public bool RememberIndexing { get; set; }
-
- /// <summary>
- /// Gets or sets the height of the primary image.
- /// </summary>
- /// <value>The height of the primary image.</value>
- public int PrimaryImageHeight { get; set; }
-
- /// <summary>
- /// Gets or sets the width of the primary image.
- /// </summary>
- /// <value>The width of the primary image.</value>
- public int PrimaryImageWidth { get; set; }
-
- /// <summary>
- /// Gets or sets the custom prefs.
- /// </summary>
- /// <value>The custom prefs.</value>
- public Dictionary<string, string> CustomPrefs { get; set; }
-
- /// <summary>
- /// Gets or sets the scroll direction.
- /// </summary>
- /// <value>The scroll direction.</value>
- public ScrollDirection ScrollDirection { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether to show backdrops on this item.
- /// </summary>
- /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value>
- public bool ShowBackdrop { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [remember sorting].
- /// </summary>
- /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value>
- public bool RememberSorting { get; set; }
-
- /// <summary>
- /// Gets or sets the sort order.
- /// </summary>
- /// <value>The sort order.</value>
- public SortOrder SortOrder { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether [show sidebar].
- /// </summary>
- /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value>
- public bool ShowSidebar { get; set; }
-
- /// <summary>
- /// Gets or sets the client.
- /// </summary>
- public string Client { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs
index 6ea9ee419..1f7e03718 100644
--- a/MediaBrowser.Model/Entities/ImageType.cs
+++ b/MediaBrowser.Model/Entities/ImageType.cs
@@ -48,6 +48,10 @@ namespace MediaBrowser.Model.Entities
/// <summary>
/// The screenshot.
/// </summary>
+ /// <remarks>
+ /// This enum value is obsolete.
+ /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
+ /// </remarks>
Screenshot = 8,
/// <summary>
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index c67f30d04..38ac44794 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -255,13 +255,18 @@ namespace MediaBrowser.Model.Entities
attributes.Add(string.IsNullOrEmpty(LocalizedForced) ? "Forced" : LocalizedForced);
}
+ if (!string.IsNullOrEmpty(Codec))
+ {
+ attributes.Add(Codec.ToUpperInvariant());
+ }
+
if (!string.IsNullOrEmpty(Title))
{
var result = new StringBuilder(Title);
foreach (var tag in attributes)
{
// Keep Tags that are not already in Title.
- if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1)
+ if (!Title.Contains(tag, StringComparison.OrdinalIgnoreCase))
{
result.Append(" - ").Append(tag);
}
@@ -469,64 +474,30 @@ namespace MediaBrowser.Model.Entities
/// <value><c>true</c> if this instance is anamorphic; otherwise, <c>false</c>.</value>
public bool? IsAnamorphic { get; set; }
- private string GetResolutionText()
+ internal string GetResolutionText()
{
- var i = this;
-
- if (i.Width.HasValue && i.Height.HasValue)
+ if (!Width.HasValue || !Height.HasValue)
{
- var width = i.Width.Value;
- var height = i.Height.Value;
-
- if (width >= 3800 || height >= 2000)
- {
- return "4K";
- }
-
- if (width >= 2500)
- {
- if (i.IsInterlaced)
- {
- return "1440i";
- }
-
- return "1440p";
- }
-
- if (width >= 1900 || height >= 1000)
- {
- if (i.IsInterlaced)
- {
- return "1080i";
- }
-
- return "1080p";
- }
-
- if (width >= 1260 || height >= 700)
- {
- if (i.IsInterlaced)
- {
- return "720i";
- }
-
- return "720p";
- }
-
- if (width >= 700 || height >= 440)
- {
- if (i.IsInterlaced)
- {
- return "480i";
- }
-
- return "480p";
- }
-
- return "SD";
+ return null;
}
- return null;
+ return Width switch
+ {
+ <= 720 when Height <= 480 => IsInterlaced ? "480i" : "480p",
+ // 720x576 (PAL) (768 when rescaled for square pixels)
+ <= 768 when Height <= 576 => IsInterlaced ? "576i" : "576p",
+ // 960x540 (sometimes 544 which is multiple of 16)
+ <= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p",
+ // 1280x720
+ <= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p",
+ // 1920x1080
+ <= 1920 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
+ // 4K
+ <= 4096 when Height <= 3072 => "4K",
+ // 8K
+ <= 8192 when Height <= 6144 => "8K",
+ _ => null
+ };
}
public static bool IsTextFormat(string format)
diff --git a/MediaBrowser.Model/Entities/PersonType.cs b/MediaBrowser.Model/Entities/PersonType.cs
index 81db9c613..b985507f0 100644
--- a/MediaBrowser.Model/Entities/PersonType.cs
+++ b/MediaBrowser.Model/Entities/PersonType.cs
@@ -1,48 +1,68 @@
namespace MediaBrowser.Model.Entities
{
/// <summary>
- /// Struct PersonType.
+ /// Types of persons.
/// </summary>
- public class PersonType
+ public static class PersonType
{
/// <summary>
- /// The actor.
+ /// A person whose profession is acting on the stage, in films, or on television.
/// </summary>
public const string Actor = "Actor";
/// <summary>
- /// The director.
+ /// A person who supervises the actors and other staff in a film, play, or similar production.
/// </summary>
public const string Director = "Director";
/// <summary>
- /// The composer.
+ /// A person who writes music, especially as a professional occupation.
/// </summary>
public const string Composer = "Composer";
/// <summary>
- /// The writer.
+ /// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity.
/// </summary>
public const string Writer = "Writer";
/// <summary>
- /// The guest star.
+ /// A well-known actor or other performer who appears in a work in which they do not have a regular role.
/// </summary>
public const string GuestStar = "GuestStar";
/// <summary>
- /// The producer.
+ /// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc.
/// </summary>
public const string Producer = "Producer";
/// <summary>
- /// The conductor.
+ /// A person who directs the performance of an orchestra or choir.
/// </summary>
public const string Conductor = "Conductor";
/// <summary>
- /// The lyricist.
+ /// A person who writes the words to a song or musical.
/// </summary>
public const string Lyricist = "Lyricist";
+
+ /// <summary>
+ /// A person who adapts a musical composition for performance.
+ /// </summary>
+ public const string Arranger = "Arranger";
+
+ /// <summary>
+ /// An audio engineer who performed a general engineering role.
+ /// </summary>
+ public const string Engineer = "Engineer";
+
+ /// <summary>
+ /// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release.
+ /// </summary>
+ public const string Mixer = "Mixer";
+
+ /// <summary>
+ /// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material.
+ /// </summary>
+ public const string Remixer = "Remixer";
}
}
diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
index 8fed392b9..2b2bda12c 100644
--- a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
+++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
@@ -3,6 +3,7 @@
using System;
using System.Text.Json.Serialization;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Model.Entities
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
index 712fa381e..a5a6b18aa 100644
--- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -18,6 +18,12 @@ namespace MediaBrowser.Model.Extensions
/// <returns>The ordered remote image infos.</returns>
public static IEnumerable<RemoteImageInfo> OrderByLanguageDescending(this IEnumerable<RemoteImageInfo> remoteImageInfos, string requestedLanguage)
{
+ if (string.IsNullOrWhiteSpace(requestedLanguage))
+ {
+ // Default to English if no requested language is specified.
+ requestedLanguage = "en";
+ }
+
var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
return remoteImageInfos.OrderByDescending(i =>
@@ -27,14 +33,16 @@ namespace MediaBrowser.Model.Extensions
return 3;
}
- if (!isRequestedLanguageEn && string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(i.Language))
{
- return 2;
+ // Assume empty image language is likely to be English.
+ return isRequestedLanguageEn ? 3 : 2;
}
- if (string.IsNullOrEmpty(i.Language))
+ if (!isRequestedLanguageEn && string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase))
{
- return isRequestedLanguageEn ? 3 : 2;
+ // Prioritize English over non-requested languages.
+ return 2;
}
return 0;
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index baefeb39c..e00157dce 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -1,6 +1,4 @@
-#nullable disable
using System.Collections.Generic;
-using System.Globalization;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Globalization
@@ -57,18 +55,10 @@ namespace MediaBrowser.Model.Globalization
IEnumerable<LocalizationOption> GetLocalizationOptions();
/// <summary>
- /// Checks if the string contains a character with the specified unicode category.
- /// </summary>
- /// <param name="value">The string.</param>
- /// <param name="category">The unicode category.</param>
- /// <returns>Wether or not the string contains a character with the specified unicode category.</returns>
- bool HasUnicodeCategory(string value, UnicodeCategory category);
-
- /// <summary>
- /// Returns the correct <see cref="CultureInfo" /> for the given language.
+ /// Returns the correct <see cref="CultureDto" /> for the given language.
/// </summary>
/// <param name="language">The language.</param>
- /// <returns>The correct <see cref="CultureInfo" /> for the given language.</returns>
- CultureDto FindLanguageInfo(string language);
+ /// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
+ CultureDto? FindLanguageInfo(string language);
}
}
diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs
new file mode 100644
index 000000000..3c8007d1c
--- /dev/null
+++ b/MediaBrowser.Model/IO/AsyncFile.cs
@@ -0,0 +1,45 @@
+using System.IO;
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Helper class to create async <see cref="FileStream" />s.
+ /// </summary>
+ public static class AsyncFile
+ {
+ /// <summary>
+ /// Gets the default <see cref="FileStreamOptions"/> for reading files async.
+ /// </summary>
+ public static FileStreamOptions ReadOptions => new FileStreamOptions()
+ {
+ Options = FileOptions.Asynchronous
+ };
+
+ /// <summary>
+ /// Gets the default <see cref="FileStreamOptions"/> for writing files async.
+ /// </summary>
+ public static FileStreamOptions WriteOptions => new FileStreamOptions()
+ {
+ Mode = FileMode.OpenOrCreate,
+ Access = FileAccess.Write,
+ Share = FileShare.None,
+ Options = FileOptions.Asynchronous
+ };
+
+ /// <summary>
+ /// Opens an existing file for reading.
+ /// </summary>
+ /// <param name="path">The file to be opened for reading.</param>
+ /// <returns>A read-only <see cref="FileStream" /> on the specified path.</returns>
+ public static FileStream OpenRead(string path)
+ => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+
+ /// <summary>
+ /// Opens an existing file for writing.
+ /// </summary>
+ /// <param name="path">The file to be opened for writing.</param>
+ /// <returns>An unshared <see cref="FileStream" /> object on the specified path with Write access.</returns>
+ public static FileStream OpenWrite(string path)
+ => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ }
+}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index be4f1e16b..0f77d6b5b 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -51,7 +51,7 @@ namespace MediaBrowser.Model.IO
/// <returns>A <see cref="FileSystemMetadata" /> object.</returns>
/// <remarks><para>If the specified path points to a directory, the returned <see cref="FileSystemMetadata" /> object's
/// <see cref="FileSystemMetadata.IsDirectory" /> property and the <see cref="FileSystemMetadata.Exists" /> property will both be set to false.</para>
- /// <para>For automatic handling of files <b>and</b> directories, use <see cref="M:IFileSystem.GetFileSystemInfo(System.String)" />.</para></remarks>
+ /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo(string)" />.</para></remarks>
FileSystemMetadata GetFileInfo(string path);
/// <summary>
@@ -61,7 +61,7 @@ namespace MediaBrowser.Model.IO
/// <returns>A <see cref="FileSystemMetadata" /> object.</returns>
/// <remarks><para>If the specified path points to a file, the returned <see cref="FileSystemMetadata" /> object's
/// <see cref="FileSystemMetadata.IsDirectory" /> property will be set to true and the <see cref="FileSystemMetadata.Exists" /> property will be set to false.</para>
- /// <para>For automatic handling of files <b>and</b> directories, use <see cref="M:IFileSystem.GetFileSystemInfo(System.String)" />.</para></remarks>
+ /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo(string)" />.</para></remarks>
FileSystemMetadata GetDirectoryInfo(string path);
/// <summary>
diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs
index fca52ebae..2448575d1 100644
--- a/MediaBrowser.Model/IO/IZipClient.cs
+++ b/MediaBrowser.Model/IO/IZipClient.cs
@@ -9,64 +9,8 @@ namespace MediaBrowser.Model.IO
/// </summary>
public interface IZipClient
{
- /// <summary>
- /// Extracts all.
- /// </summary>
- /// <param name="sourceFile">The source file.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles);
-
- /// <summary>
- /// Extracts all.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles);
-
void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles);
void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName);
-
- /// <summary>
- /// Extracts all from zip.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles);
-
- /// <summary>
- /// Extracts all from7z.
- /// </summary>
- /// <param name="sourceFile">The source file.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles);
-
- /// <summary>
- /// Extracts all from7z.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles);
-
- /// <summary>
- /// Extracts all from tar.
- /// </summary>
- /// <param name="sourceFile">The source file.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles);
-
- /// <summary>
- /// Extracts all from tar.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="targetPath">The target path.</param>
- /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
- void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles);
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 4db99f0b0..a161b99fd 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -14,13 +14,9 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <!-- <AnalysisMode>AllEnabledByDefault</AnalysisMode> -->
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
@@ -33,10 +29,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
+ <PackageReference Include="MimeTypes" Version="2.2.1">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
<PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="5.0.2" />
+ <PackageReference Include="System.Text.Json" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
@@ -50,7 +50,8 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
+ <ProjectReference Include="../Jellyfin.Data/Jellyfin.Data.csproj" />
+ <ProjectReference Include="../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.Model/MediaInfo/AudioCodec.cs b/MediaBrowser.Model/MediaInfo/AudioCodec.cs
index 8b17757b8..7b83b1b9d 100644
--- a/MediaBrowser.Model/MediaInfo/AudioCodec.cs
+++ b/MediaBrowser.Model/MediaInfo/AudioCodec.cs
@@ -1,13 +1,11 @@
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.MediaInfo
{
public static class AudioCodec
{
- public const string AAC = "aac";
- public const string MP3 = "mp3";
- public const string AC3 = "ac3";
-
public static string GetFriendlyName(string codec)
{
if (codec.Length == 0)
@@ -15,17 +13,20 @@ namespace MediaBrowser.Model.MediaInfo
return codec;
}
- switch (codec.ToLowerInvariant())
+ if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "Dolby Digital";
+ }
+ else if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
{
- case "ac3":
- return "Dolby Digital";
- case "eac3":
- return "Dolby Digital+";
- case "dca":
- return "DTS";
- default:
- return codec.ToUpperInvariant();
+ return "Dolby Digital+";
}
+ else if (string.Equals(codec, "dca", StringComparison.OrdinalIgnoreCase))
+ {
+ return "DTS";
+ }
+
+ return codec.ToUpperInvariant();
}
}
}
diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
index 36a240706..24eab1a74 100644
--- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
+++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
@@ -16,22 +16,6 @@ namespace MediaBrowser.Model.MediaInfo
DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
}
- public LiveStreamRequest(AudioOptions options)
- {
- MaxStreamingBitrate = options.MaxBitrate;
- ItemId = options.ItemId;
- DeviceProfile = options.Profile;
- MaxAudioChannels = options.MaxAudioChannels;
-
- DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
-
- if (options is VideoOptions videoOptions)
- {
- AudioStreamIndex = videoOptions.AudioStreamIndex;
- SubtitleStreamIndex = videoOptions.SubtitleStreamIndex;
- }
- }
-
public string OpenToken { get; set; }
public Guid UserId { get; set; }
diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
deleted file mode 100644
index ecd9b8834..000000000
--- a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-#nullable disable
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.Dlna;
-
-namespace MediaBrowser.Model.MediaInfo
-{
- public class PlaybackInfoRequest
- {
- public PlaybackInfoRequest()
- {
- EnableDirectPlay = true;
- EnableDirectStream = true;
- EnableTranscoding = true;
- AllowVideoStreamCopy = true;
- AllowAudioStreamCopy = true;
- IsPlayback = true;
- DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
- }
-
- public Guid Id { get; set; }
-
- public Guid UserId { get; set; }
-
- public long? MaxStreamingBitrate { get; set; }
-
- public long? StartTimeTicks { get; set; }
-
- public int? AudioStreamIndex { get; set; }
-
- public int? SubtitleStreamIndex { get; set; }
-
- public int? MaxAudioChannels { get; set; }
-
- public string MediaSourceId { get; set; }
-
- public string LiveStreamId { get; set; }
-
- public DeviceProfile DeviceProfile { get; set; }
-
- public bool EnableDirectPlay { get; set; }
-
- public bool EnableDirectStream { get; set; }
-
- public bool EnableTranscoding { get; set; }
-
- public bool AllowVideoStreamCopy { get; set; }
-
- public bool AllowAudioStreamCopy { get; set; }
-
- public bool IsPlayback { get; set; }
-
- public bool AutoOpenLiveStream { get; set; }
-
- public MediaProtocol[] DirectPlayProtocols { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs
index 2bd45695a..9bc5c31f6 100644
--- a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs
+++ b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs
@@ -8,8 +8,6 @@ namespace MediaBrowser.Model.MediaInfo
public const string SSA = "ssa";
public const string ASS = "ass";
public const string VTT = "vtt";
- public const string SUB = "sub";
- public const string SMI = "smi";
public const string TTML = "ttml";
}
}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 96f5ab51a..3b03466e9 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -2,14 +2,25 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Net
{
/// <summary>
/// Class MimeTypes.
/// </summary>
+ ///
+ /// <remarks>
+ /// For more information on MIME types:
+ /// <list type="bullet">
+ /// <item>http://en.wikipedia.org/wiki/Internet_media_type</item>
+ /// <item>https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types</item>
+ /// <item>http://www.iana.org/assignments/media-types/media-types.xhtml</item>
+ /// </list>
+ /// </remarks>
public static class MimeTypes
{
/// <summary>
@@ -48,81 +59,26 @@ namespace MediaBrowser.Model.Net
".wtv",
};
- // http://en.wikipedia.org/wiki/Internet_media_type
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
- // http://www.iana.org/assignments/media-types/media-types.xhtml
- // Add more as needed
+ /// <summary>
+ /// Used for extensions not in <see cref="Model.MimeTypes"/> or to override them.
+ /// </summary>
private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// Type application
- { ".7z", "application/x-7z-compressed" },
- { ".azw", "application/vnd.amazon.ebook" },
{ ".azw3", "application/vnd.amazon.ebook" },
- { ".cbz", "application/x-cbz" },
- { ".cbr", "application/epub+zip" },
- { ".eot", "application/vnd.ms-fontobject" },
- { ".epub", "application/epub+zip" },
- { ".js", "application/x-javascript" },
- { ".json", "application/json" },
- { ".m3u8", "application/x-mpegURL" },
- { ".map", "application/x-javascript" },
- { ".mobi", "application/x-mobipocket-ebook" },
- { ".opf", "application/oebps-package+xml" },
- { ".pdf", "application/pdf" },
- { ".rar", "application/vnd.rar" },
- { ".srt", "application/x-subrip" },
- { ".ttml", "application/ttml+xml" },
- { ".wasm", "application/wasm" },
- { ".xml", "application/xml" },
- { ".zip", "application/zip" },
// Type image
- { ".bmp", "image/bmp" },
- { ".gif", "image/gif" },
- { ".ico", "image/vnd.microsoft.icon" },
- { ".jpg", "image/jpeg" },
- { ".jpeg", "image/jpeg" },
- { ".png", "image/png" },
- { ".svg", "image/svg+xml" },
- { ".svgz", "image/svg+xml" },
{ ".tbn", "image/jpeg" },
- { ".tif", "image/tiff" },
- { ".tiff", "image/tiff" },
- { ".webp", "image/webp" },
-
- // Type font
- { ".ttf", "font/ttf" },
- { ".woff", "font/woff" },
- { ".woff2", "font/woff2" },
// Type text
{ ".ass", "text/x-ssa" },
{ ".ssa", "text/x-ssa" },
- { ".css", "text/css" },
- { ".csv", "text/csv" },
{ ".edl", "text/plain" },
- { ".rtf", "text/rtf" },
- { ".txt", "text/plain" },
- { ".vtt", "text/vtt" },
+ { ".html", "text/html; charset=UTF-8" },
+ { ".htm", "text/html; charset=UTF-8" },
// Type video
- { ".3gp", "video/3gpp" },
- { ".3g2", "video/3gpp2" },
- { ".asf", "video/x-ms-asf" },
- { ".avi", "video/x-msvideo" },
- { ".flv", "video/x-flv" },
- { ".mp4", "video/mp4" },
- { ".m4s", "video/mp4" },
- { ".m4v", "video/x-m4v" },
{ ".mpegts", "video/mp2t" },
- { ".mpg", "video/mpeg" },
- { ".mkv", "video/x-matroska" },
- { ".mov", "video/quicktime" },
- { ".mpd", "video/vnd.mpeg.dash.mpd" },
- { ".ogv", "video/ogg" },
- { ".ts", "video/mp2t" },
- { ".webm", "video/webm" },
- { ".wmv", "video/x-ms-wmv" },
// Type audio
{ ".aac", "audio/aac" },
@@ -131,47 +87,58 @@ namespace MediaBrowser.Model.Net
{ ".dsf", "audio/dsf" },
{ ".dsp", "audio/dsp" },
{ ".flac", "audio/flac" },
- { ".m4a", "audio/mp4" },
{ ".m4b", "audio/m4b" },
- { ".mid", "audio/midi" },
- { ".midi", "audio/midi" },
{ ".mp3", "audio/mpeg" },
- { ".oga", "audio/ogg" },
- { ".ogg", "audio/ogg" },
- { ".opus", "audio/ogg" },
{ ".vorbis", "audio/vorbis" },
- { ".wav", "audio/wav" },
{ ".webma", "audio/webm" },
- { ".wma", "audio/x-ms-wma" },
{ ".wv", "audio/x-wavpack" },
{ ".xsp", "audio/xsp" },
};
- private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup();
-
- private static Dictionary<string, string> CreateExtensionLookup()
+ private static readonly Dictionary<string, string> _extensionLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
- var dict = _mimeTypeLookup
- .GroupBy(i => i.Value)
- .ToDictionary(x => x.Key, x => x.First().Key, StringComparer.OrdinalIgnoreCase);
+ // Type application
+ { "application/x-cbz", ".cbz" },
+ { "application/x-javascript", ".js" },
+ { "application/xml", ".xml" },
+ { "application/x-mpegURL", ".m3u8" },
+
+ // Type audio
+ { "audio/aac", ".aac" },
+ { "audio/ac3", ".ac3" },
+ { "audio/dsf", ".dsf" },
+ { "audio/dsp", ".dsp" },
+ { "audio/flac", ".flac" },
+ { "audio/m4b", ".m4b" },
+ { "audio/vorbis", ".vorbis" },
+ { "audio/x-ape", ".ape" },
+ { "audio/xsp", ".xsp" },
+ { "audio/x-wavpack", ".wv" },
- dict["image/jpg"] = ".jpg";
- dict["image/x-png"] = ".png";
+ // Type image
+ { "image/jpeg", ".jpg" },
+ { "image/x-png", ".png" },
- dict["audio/x-aac"] = ".aac";
+ // Type text
+ { "text/plain", ".txt" },
+ { "text/rtf", ".rtf" },
+ { "text/x-ssa", ".ssa" },
- return dict;
- }
+ // Type video
+ { "video/vnd.mpeg.dash.mpd", ".mpd" },
+ { "video/x-matroska", ".mkv" },
+ };
- public static string? GetMimeType(string path) => GetMimeType(path, true);
+ public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
/// <summary>
/// Gets the type of the MIME.
/// </summary>
/// <param name="filename">The filename to find the MIME type of.</param>
- /// <param name="enableStreamDefault">Whether of not to return a default value if no fitting MIME type is found.</param>
- /// <returns>The worrect MIME type for the given filename, or `null` if it wasn't found and <paramref name="enableStreamDefault"/> is false.</returns>
- public static string? GetMimeType(string filename, bool enableStreamDefault)
+ /// <param name="defaultValue">The default value to return if no fitting MIME type is found.</param>
+ /// <returns>The correct MIME type for the given filename, or <paramref name="defaultValue"/> if it wasn't found.</returns>
+ [return: NotNullIfNotNull("defaultValue")]
+ public static string? GetMimeType(string filename, string? defaultValue = null)
{
if (filename.Length == 0)
{
@@ -185,32 +152,18 @@ namespace MediaBrowser.Model.Net
return result;
}
- // Catch-all for all video types that don't require specific mime types
- if (_videoFileExtensions.Contains(ext))
- {
- return "video/" + ext.Substring(1);
- }
-
- // Type text
- if (string.Equals(ext, ".html", StringComparison.OrdinalIgnoreCase)
- || string.Equals(ext, ".htm", StringComparison.OrdinalIgnoreCase))
+ if (Model.MimeTypes.TryGetMimeType(filename, out var mimeType))
{
- return "text/html; charset=UTF-8";
+ return mimeType;
}
- if (string.Equals(ext, ".log", StringComparison.OrdinalIgnoreCase)
- || string.Equals(ext, ".srt", StringComparison.OrdinalIgnoreCase))
- {
- return "text/plain";
- }
-
- // Misc
- if (string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase))
+ // Catch-all for all video types that don't require specific mime types
+ if (_videoFileExtensions.Contains(ext))
{
- return "application/octet-stream";
+ return string.Concat("video/", ext.AsSpan(1));
}
- return enableStreamDefault ? "application/octet-stream" : null;
+ return defaultValue;
}
public static string? ToExtension(string mimeType)
@@ -221,14 +174,15 @@ namespace MediaBrowser.Model.Net
}
// handle text/html; charset=UTF-8
- mimeType = mimeType.Split(';')[0];
+ mimeType = mimeType.AsSpan().LeftPart(';').ToString();
if (_extensionLookup.TryGetValue(mimeType, out string? result))
{
return result;
}
- return null;
+ var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
+ return string.IsNullOrEmpty(extension) ? null : "." + extension;
}
}
}
diff --git a/MediaBrowser.Model/Net/NetworkShareType.cs b/MediaBrowser.Model/Net/NetworkShareType.cs
deleted file mode 100644
index 5d985f85d..000000000
--- a/MediaBrowser.Model/Net/NetworkShareType.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace MediaBrowser.Model.Net
-{
- /// <summary>
- /// Enum NetworkShareType.
- /// </summary>
- public enum NetworkShareType
- {
- /// <summary>
- /// Disk share.
- /// </summary>
- Disk,
-
- /// <summary>
- /// Printer share.
- /// </summary>
- Printer,
-
- /// <summary>
- /// Device share.
- /// </summary>
- Device,
-
- /// <summary>
- /// IPC share.
- /// </summary>
- Ipc,
-
- /// <summary>
- /// Special share.
- /// </summary>
- Special
- }
-}
diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs
index 09beb2ef7..d1b5491bd 100644
--- a/MediaBrowser.Model/Notifications/NotificationOptions.cs
+++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs
@@ -2,9 +2,9 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
namespace MediaBrowser.Model.Notifications
{
@@ -94,7 +94,7 @@ namespace MediaBrowser.Model.Notifications
NotificationOption opt = GetOptions(notificationType);
return opt == null
- || !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
+ || !opt.DisabledServices.Contains(service, StringComparison.OrdinalIgnoreCase);
}
public bool IsEnabledToMonitorUser(string type, Guid userId)
@@ -103,7 +103,7 @@ namespace MediaBrowser.Model.Notifications
return opt != null
&& opt.Enabled
- && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparer.OrdinalIgnoreCase);
+ && !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparison.OrdinalIgnoreCase);
}
public bool IsEnabledToSendToUser(string type, string userId, User user)
@@ -122,7 +122,7 @@ namespace MediaBrowser.Model.Notifications
return true;
}
- return opt.SendToUsers.Contains(userId, StringComparer.OrdinalIgnoreCase);
+ return opt.SendToUsers.Contains(userId, StringComparison.OrdinalIgnoreCase);
}
return false;
diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs
index f99e9ece9..e50baf604 100644
--- a/MediaBrowser.Model/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Jellyfin.Model.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
index 0ea3e96ca..d026d574f 100644
--- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs
+++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
@@ -12,7 +12,7 @@ namespace MediaBrowser.Model.Providers
/// <param name="key">Key for this id. This key should be unique across all providers.</param>
/// <param name="type">Specific media type for this id.</param>
/// <param name="urlFormatString">URL format string.</param>
- public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string urlFormatString)
+ public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString)
{
Name = name;
Key = key;
@@ -46,6 +46,6 @@ namespace MediaBrowser.Model.Providers
/// <summary>
/// Gets or sets the URL format string.
/// </summary>
- public string UrlFormatString { get; set; }
+ public string? UrlFormatString { get; set; }
}
}
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index ef4698f3f..e6c3a6c26 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -1,5 +1,7 @@
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.Querying
{
/// <summary>
@@ -143,6 +145,7 @@ namespace MediaBrowser.Model.Querying
/// <summary>
/// The screenshot image tags.
/// </summary>
+ [Obsolete("Screenshot image type is no longer used.")]
ScreenshotImageTags,
SeriesPrimaryImage,
diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
index f555ffb36..d2d9f1f9a 100644
--- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs
+++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Querying
@@ -48,7 +49,7 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets the include item types.
/// </summary>
/// <value>The include item types.</value>
- public string[] IncludeItemTypes { get; set; }
+ public BaseItemKind[] IncludeItemTypes { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is played.
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
index 0fa40b6a7..35a82f47c 100644
--- a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
+++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs
@@ -3,38 +3,76 @@ using System;
namespace MediaBrowser.Model.QuickConnect
{
/// <summary>
- /// Stores the result of an incoming quick connect request.
+ /// Stores the state of an quick connect request.
/// </summary>
public class QuickConnectResult
{
/// <summary>
- /// Gets a value indicating whether this request is authorized.
+ /// Initializes a new instance of the <see cref="QuickConnectResult"/> class.
/// </summary>
- public bool Authenticated => !string.IsNullOrEmpty(Authentication);
+ /// <param name="secret">The secret used to query the request state.</param>
+ /// <param name="code">The code used to allow the request.</param>
+ /// <param name="dateAdded">The time when the request was created.</param>
+ /// <param name="deviceId">The requesting device id.</param>
+ /// <param name="deviceName">The requesting device name.</param>
+ /// <param name="appName">The requesting app name.</param>
+ /// <param name="appVersion">The requesting app version.</param>
+ public QuickConnectResult(
+ string secret,
+ string code,
+ DateTime dateAdded,
+ string deviceId,
+ string deviceName,
+ string appName,
+ string appVersion)
+ {
+ Secret = secret;
+ Code = code;
+ DateAdded = dateAdded;
+ DeviceId = deviceId;
+ DeviceName = deviceName;
+ AppName = appName;
+ AppVersion = appVersion;
+ }
/// <summary>
- /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
+ /// Gets or sets a value indicating whether this request is authorized.
/// </summary>
- public string? Secret { get; set; }
+ public bool Authenticated { get; set; }
/// <summary>
- /// Gets or sets the user facing code used so the user can quickly differentiate this request from others.
+ /// Gets the secret value used to uniquely identify this request. Can be used to retrieve authentication information.
/// </summary>
- public string? Code { get; set; }
+ public string Secret { get; }
/// <summary>
- /// Gets or sets the private access token.
+ /// Gets the user facing code used so the user can quickly differentiate this request from others.
/// </summary>
- public string? Authentication { get; set; }
+ public string Code { get; }
/// <summary>
- /// Gets or sets an error message.
+ /// Gets the requesting device id.
/// </summary>
- public string? Error { get; set; }
+ public string DeviceId { get; }
+
+ /// <summary>
+ /// Gets the requesting device name.
+ /// </summary>
+ public string DeviceName { get; }
+
+ /// <summary>
+ /// Gets the requesting app name.
+ /// </summary>
+ public string AppName { get; }
+
+ /// <summary>
+ /// Gets the requesting app version.
+ /// </summary>
+ public string AppVersion { get; }
/// <summary>
/// Gets or sets the DateTime that this request was created.
/// </summary>
- public DateTime? DateAdded { get; set; }
+ public DateTime DateAdded { get; set; }
}
}
diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs
deleted file mode 100644
index f1074f25f..000000000
--- a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace MediaBrowser.Model.QuickConnect
-{
- /// <summary>
- /// Quick connect state.
- /// </summary>
- public enum QuickConnectState
- {
- /// <summary>
- /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in.
- /// </summary>
- Unavailable = 0,
-
- /// <summary>
- /// The feature is enabled for use on the server but is not currently accepting connection requests.
- /// </summary>
- Available = 1,
-
- /// <summary>
- /// The feature is actively accepting connection requests.
- /// </summary>
- Active = 2
- }
-}
diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs
index aedfa4d36..1caed827f 100644
--- a/MediaBrowser.Model/Search/SearchQuery.cs
+++ b/MediaBrowser.Model/Search/SearchQuery.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Search
{
@@ -16,8 +17,8 @@ namespace MediaBrowser.Model.Search
IncludeStudios = true;
MediaTypes = Array.Empty<string>();
- IncludeItemTypes = Array.Empty<string>();
- ExcludeItemTypes = Array.Empty<string>();
+ IncludeItemTypes = Array.Empty<BaseItemKind>();
+ ExcludeItemTypes = Array.Empty<BaseItemKind>();
}
/// <summary>
@@ -56,9 +57,9 @@ namespace MediaBrowser.Model.Search
public string[] MediaTypes { get; set; }
- public string[] IncludeItemTypes { get; set; }
+ public BaseItemKind[] IncludeItemTypes { get; set; }
- public string[] ExcludeItemTypes { get; set; }
+ public BaseItemKind[] ExcludeItemTypes { get; set; }
public Guid? ParentId { get; set; }
diff --git a/MediaBrowser.Model/Session/BrowseRequest.cs b/MediaBrowser.Model/Session/BrowseRequest.cs
index 65afe5cf3..5ad7d783a 100644
--- a/MediaBrowser.Model/Session/BrowseRequest.cs
+++ b/MediaBrowser.Model/Session/BrowseRequest.cs
@@ -1,3 +1,5 @@
+using Jellyfin.Data.Enums;
+
#nullable disable
namespace MediaBrowser.Model.Session
{
@@ -8,10 +10,9 @@ namespace MediaBrowser.Model.Session
{
/// <summary>
/// Gets or sets the item type.
- /// Artist, Genre, Studio, Person, or any kind of BaseItem.
/// </summary>
/// <value>The type of the item.</value>
- public string ItemType { get; set; }
+ public BaseItemKind ItemType { get; set; }
/// <summary>
/// Gets or sets the item id.
diff --git a/MediaBrowser.Model/Session/HardwareEncodingType.cs b/MediaBrowser.Model/Session/HardwareEncodingType.cs
new file mode 100644
index 000000000..0e172f35f
--- /dev/null
+++ b/MediaBrowser.Model/Session/HardwareEncodingType.cs
@@ -0,0 +1,48 @@
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Enum HardwareEncodingType.
+ /// </summary>
+ public enum HardwareEncodingType
+ {
+ /// <summary>
+ /// AMD AMF
+ /// </summary>
+ AMF = 0,
+
+ /// <summary>
+ /// Intel Quick Sync Video
+ /// </summary>
+ QSV = 1,
+
+ /// <summary>
+ /// NVIDIA NVENC
+ /// </summary>
+ NVENC = 2,
+
+ /// <summary>
+ /// OpenMax OMX
+ /// </summary>
+ OMX = 3,
+
+ /// <summary>
+ /// Exynos V4L2 MFC
+ /// </summary>
+ V4L2M2M = 4,
+
+ /// <summary>
+ /// MediaCodec Android
+ /// </summary>
+ MediaCodec = 5,
+
+ /// <summary>
+ /// Video Acceleration API (VAAPI)
+ /// </summary>
+ VAAPI = 6,
+
+ /// <summary>
+ /// Video ToolBox
+ /// </summary>
+ VideoToolBox = 7
+ }
+}
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
index e93b5d288..3c95df66d 100644
--- a/MediaBrowser.Model/Session/TranscodeReason.cs
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -26,6 +26,7 @@ namespace MediaBrowser.Model.Session
VideoProfileNotSupported = 19,
AudioBitDepthNotSupported = 20,
SubtitleCodecNotSupported = 21,
- DirectPlayError = 22
+ DirectPlayError = 22,
+ AudioIsExternal = 23
}
}
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
index 064a087d5..68ab691f8 100644
--- a/MediaBrowser.Model/Session/TranscodingInfo.cs
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -34,6 +34,8 @@ namespace MediaBrowser.Model.Session
public int? AudioChannels { get; set; }
+ public HardwareEncodingType? HardwareAccelerationType { get; set; }
+
public TranscodeReason[] TranscodeReasons { get; set; }
}
}
diff --git a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
index a851229f7..cce99c77d 100644
--- a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
@@ -16,15 +16,17 @@ namespace MediaBrowser.Model.SyncPlay
/// <param name="playlist">The playlist.</param>
/// <param name="playingItemIndex">The playing item index in the playlist.</param>
/// <param name="startPositionTicks">The start position ticks.</param>
+ /// <param name="isPlaying">The playing item status.</param>
/// <param name="shuffleMode">The shuffle mode.</param>
/// <param name="repeatMode">The repeat mode.</param>
- public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
+ public PlayQueueUpdate(PlayQueueUpdateReason reason, DateTime lastUpdate, IReadOnlyList<QueueItem> playlist, int playingItemIndex, long startPositionTicks, bool isPlaying, GroupShuffleMode shuffleMode, GroupRepeatMode repeatMode)
{
Reason = reason;
LastUpdate = lastUpdate;
Playlist = playlist;
PlayingItemIndex = playingItemIndex;
StartPositionTicks = startPositionTicks;
+ IsPlaying = isPlaying;
ShuffleMode = shuffleMode;
RepeatMode = repeatMode;
}
@@ -60,6 +62,12 @@ namespace MediaBrowser.Model.SyncPlay
public long StartPositionTicks { get; }
/// <summary>
+ /// Gets a value indicating whether the current item is playing.
+ /// </summary>
+ /// <value>The playing item status.</value>
+ public bool IsPlaying { get; }
+
+ /// <summary>
/// Gets the shuffle mode.
/// </summary>
/// <value>The shuffle mode.</value>
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
index d75ae91c0..a82c1c8c0 100644
--- a/MediaBrowser.Model/System/SystemInfo.cs
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -130,8 +130,10 @@ namespace MediaBrowser.Model.System
/// 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>
+ [Obsolete("This should be handled by the package manager")]
public bool HasUpdateAvailable { get; set; }
+ [Obsolete("This isn't set correctly anymore")]
public FFmpegLocation EncoderLocation { get; set; }
public Architecture SystemArchitecture { get; set; }
diff --git a/MediaBrowser.Model/Users/PinRedeemResult.cs b/MediaBrowser.Model/Users/PinRedeemResult.cs
index 7e4553bac..23fa631e8 100644
--- a/MediaBrowser.Model/Users/PinRedeemResult.cs
+++ b/MediaBrowser.Model/Users/PinRedeemResult.cs
@@ -1,6 +1,7 @@
-#nullable disable
#pragma warning disable CS1591
+using System;
+
namespace MediaBrowser.Model.Users
{
public class PinRedeemResult
@@ -15,6 +16,6 @@ namespace MediaBrowser.Model.Users
/// Gets or sets the users reset.
/// </summary>
/// <value>The users reset.</value>
- public string[] UsersReset { get; set; }
+ public string[] UsersReset { get; set; } = Array.Empty<string>();
}
}
diff --git a/MediaBrowser.Model/Users/UserActionType.cs b/MediaBrowser.Model/Users/UserActionType.cs
deleted file mode 100644
index dbb1513f2..000000000
--- a/MediaBrowser.Model/Users/UserActionType.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.Users
-{
- public enum UserActionType
- {
- PlayedItem = 0
- }
-}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 111070d81..3634d0705 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -1,5 +1,5 @@
#nullable disable
-#pragma warning disable CS1591
+#pragma warning disable CS1591, CA1819
using System;
using System.Xml.Serialization;
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
index e5326da71..88ce8d087 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
@@ -59,9 +59,9 @@ namespace MediaBrowser.Providers.BoxSets
}
/// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType)
{
- var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
var libraryFolderIds = item.GetLibraryFolderIds();
@@ -69,10 +69,10 @@ namespace MediaBrowser.Providers.BoxSets
if (itemLibraryFolderIds == null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds))
{
item.LibraryFolderIds = libraryFolderIds;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- return updateType;
+ return updatedType;
}
}
}
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index fb1d4f490..4632e1d51 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -29,8 +32,6 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
public class ImageSaver
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
/// <summary>
/// The _config.
/// </summary>
@@ -91,7 +92,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(mimeType));
}
- var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && !(item is Audio);
+ var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && item is not Audio;
if (type != ImageType.Primary && item is Episode)
{
@@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.Manager
// Delete the current path
if (currentImageIsLocalFile
- && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase)
+ && !savedPaths.Contains(currentImagePath, StringComparison.OrdinalIgnoreCase)
&& (saveLocally || currentImagePath.Contains(_config.ApplicationPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase)))
{
var currentPath = currentImagePath;
@@ -263,8 +264,10 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = source.Length;
+ await using (var fs = new FileStream(path, fileStreamOptions))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
@@ -377,7 +380,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-landscape" + extension;
@@ -400,7 +403,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-banner" + extension;
@@ -437,9 +440,6 @@ namespace MediaBrowser.Providers.Manager
case ImageType.Backdrop:
filename = GetBackdropSaveFilename(item.GetImages(type), "backdrop", "backdrop", imageIndex);
break;
- case ImageType.Screenshot:
- filename = GetBackdropSaveFilename(item.GetImages(type), "screenshot", "screenshot", imageIndex);
- break;
default:
filename = type.ToString().ToLowerInvariant();
break;
@@ -495,12 +495,12 @@ namespace MediaBrowser.Providers.Manager
var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList();
var current = 1;
- while (filenames.Contains(numberedIndexPrefix + current.ToString(UsCulture), StringComparer.OrdinalIgnoreCase))
+ while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
{
current++;
}
- return numberedIndexPrefix + current.ToString(UsCulture);
+ return numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
@@ -539,7 +539,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
@@ -556,7 +556,7 @@ namespace MediaBrowser.Providers.Manager
if (item.IsInMixedFolder)
{
- return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) };
+ return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(CultureInfo.InvariantCulture), extension) };
}
var extraFanartFilename = GetBackdropSaveFilename(item.GetImages(ImageType.Backdrop), "fanart", "fanart", outputIndex);
@@ -568,7 +568,7 @@ namespace MediaBrowser.Providers.Manager
if (EnableExtraThumbsDuplication)
{
- list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(UsCulture) + extension));
+ list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(CultureInfo.InvariantCulture) + extension));
}
return list.ToArray();
@@ -582,7 +582,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-poster" + extension;
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 966a3d822..b1d73c4c4 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -23,6 +23,9 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Manager
{
+ /// <summary>
+ /// Utilities for managing images attached to items.
+ /// </summary>
public class ItemImageProvider
{
private readonly ILogger _logger;
@@ -45,6 +48,12 @@ namespace MediaBrowser.Providers.Manager
ImageType.Thumb
};
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemImageProvider"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="providerManager">The provider manager for interacting with provider image references.</param>
+ /// <param name="fileSystem">The filesystem.</param>
public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
{
_logger = logger;
@@ -52,11 +61,18 @@ namespace MediaBrowser.Providers.Manager
_fileSystem = fileSystem;
}
+ /// <summary>
+ /// Verifies existing images have valid paths and adds any new local images provided.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to validate images for.</param>
+ /// <param name="providers">The providers to use, must include <see cref="ILocalImageProvider"/>(s) for local scanning.</param>
+ /// <param name="directoryService">The directory service for <see cref="ILocalImageProvider"/>s to use.</param>
+ /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, IDirectoryService directoryService)
{
var hasChanges = false;
- if (!(item is Photo))
+ if (item is not Photo)
{
var images = providers.OfType<ILocalImageProvider>()
.SelectMany(i => i.GetImages(item, directoryService))
@@ -71,6 +87,15 @@ namespace MediaBrowser.Providers.Manager
return hasChanges;
}
+ /// <summary>
+ /// Refreshes from the providers according to the given options.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to gather images for.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="providers">The providers to query for images.</param>
+ /// <param name="refreshOptions">The refresh options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The refresh result.</returns>
public async Task<RefreshResult> RefreshImages(
BaseItem item,
LibraryOptions libraryOptions,
@@ -78,14 +103,10 @@ namespace MediaBrowser.Providers.Manager
ImageRefreshOptions refreshOptions,
CancellationToken cancellationToken)
{
+ var oldBackdropImages = Array.Empty<ItemImageInfo>();
if (refreshOptions.IsReplacingImage(ImageType.Backdrop))
{
- ClearImages(item, ImageType.Backdrop);
- }
-
- if (refreshOptions.IsReplacingImage(ImageType.Screenshot))
- {
- ClearImages(item, ImageType.Screenshot);
+ oldBackdropImages = item.GetImages(ImageType.Backdrop).ToArray();
}
var result = new RefreshResult { UpdateType = ItemUpdateType.None };
@@ -93,16 +114,15 @@ namespace MediaBrowser.Providers.Manager
var typeName = item.GetType().Name;
var typeOptions = libraryOptions.GetTypeOptions(typeName) ?? new TypeOptions { Type = typeName };
- // In order to avoid duplicates, only download these if there are none already
- var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop);
- var screenshotLimit = typeOptions.GetLimit(ImageType.Screenshot);
+ // track library limits, adding buffer to allow lazy replacing of current images
+ var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop) + oldBackdropImages.Length;
var downloadedImages = new List<ImageType>();
foreach (var provider in providers)
{
if (provider is IRemoteImageProvider remoteProvider)
{
- await RefreshFromProvider(item, libraryOptions, remoteProvider, refreshOptions, typeOptions, backdropLimit, screenshotLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false);
+ await RefreshFromProvider(item, remoteProvider, refreshOptions, typeOptions, backdropLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false);
continue;
}
@@ -112,11 +132,17 @@ namespace MediaBrowser.Providers.Manager
}
}
+ // only delete existing multi-images if new ones were added
+ if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
+ {
+ PruneImages(item, oldBackdropImages);
+ }
+
return result;
}
/// <summary>
- /// Refreshes from provider.
+ /// Refreshes from a dynamic provider.
/// </summary>
private async Task RefreshFromProvider(
BaseItem item,
@@ -151,19 +177,20 @@ namespace MediaBrowser.Providers.Manager
if (response.Protocol == MediaProtocol.Http)
{
_logger.LogDebug("Setting image url into item {0}", item.Id);
+ var index = item.AllowsMultipleImages(imageType) ? item.GetImages(imageType).Count() : 0;
item.SetImage(
new ItemImageInfo
{
Path = response.Path,
Type = imageType
},
- 0);
+ index);
}
else
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ var stream = AsyncFile.OpenRead(response.Path);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
@@ -188,7 +215,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
- _logger.LogError(ex, "Error in {provider}", provider.Name);
+ _logger.LogError(ex, "Error in {Provider}", provider.Name);
}
}
@@ -204,9 +231,8 @@ namespace MediaBrowser.Providers.Manager
/// <param name="images">The images.</param>
/// <param name="savedOptions">The saved options.</param>
/// <param name="backdropLimit">The backdrop limit.</param>
- /// <param name="screenshotLimit">The screenshot limit.</param>
/// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
- private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit)
+ private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit)
{
// Using .Any causes the creation of a DisplayClass aka. variable capture
for (var i = 0; i < _singularImages.Length; i++)
@@ -223,36 +249,27 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- if (images.Contains(ImageType.Screenshot) && item.GetImages(ImageType.Screenshot).Count() < screenshotLimit)
- {
- return false;
- }
-
return true;
}
/// <summary>
- /// Refreshes from provider.
+ /// Refreshes from a remote provider.
/// </summary>
/// <param name="item">The item.</param>
- /// <param name="libraryOptions">The library options.</param>
/// <param name="provider">The provider.</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="savedOptions">The saved options.</param>
/// <param name="backdropLimit">The backdrop limit.</param>
- /// <param name="screenshotLimit">The screenshot limit.</param>
/// <param name="downloadedImages">The downloaded images.</param>
/// <param name="result">The result.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task RefreshFromProvider(
BaseItem item,
- LibraryOptions libraryOptions,
IRemoteImageProvider provider,
ImageRefreshOptions refreshOptions,
TypeOptions savedOptions,
int backdropLimit,
- int screenshotLimit,
ICollection<ImageType> downloadedImages,
RefreshResult result,
CancellationToken cancellationToken)
@@ -266,7 +283,7 @@ namespace MediaBrowser.Providers.Manager
if (!refreshOptions.ReplaceAllImages &&
refreshOptions.ReplaceImages.Length == 0 &&
- ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit, screenshotLimit))
+ ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit))
{
return;
}
@@ -277,7 +294,7 @@ namespace MediaBrowser.Providers.Manager
item,
new RemoteImageQuery(provider.Name)
{
- IncludeAllLanguages = false,
+ IncludeAllLanguages = true,
IncludeDisabledProviders = false,
},
cancellationToken).ConfigureAwait(false);
@@ -295,7 +312,7 @@ namespace MediaBrowser.Providers.Manager
if (!HasImage(item, imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
{
minWidth = savedOptions.GetMinWidth(imageType);
- var downloaded = await DownloadImage(item, libraryOptions, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false);
+ var downloaded = await DownloadImage(item, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false);
if (downloaded)
{
@@ -305,13 +322,7 @@ namespace MediaBrowser.Providers.Manager
}
minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
- await DownloadBackdrops(item, libraryOptions, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
-
- if (item is IHasScreenshots hasScreenshots)
- {
- minWidth = savedOptions.GetMinWidth(ImageType.Screenshot);
- await DownloadBackdrops(item, libraryOptions, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
- }
+ await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -320,7 +331,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
- _logger.LogError(ex, "Error in {provider}", provider.Name);
+ _logger.LogError(ex, "Error in {Provider}", provider.Name);
}
}
@@ -329,40 +340,36 @@ namespace MediaBrowser.Providers.Manager
return options.IsEnabled(type);
}
- private void ClearImages(BaseItem item, ImageType type)
+ private void PruneImages(BaseItem item, ItemImageInfo[] images)
{
- var deleted = false;
- var deletedImages = new List<ItemImageInfo>();
-
- foreach (var image in item.GetImages(type))
+ for (var i = 0; i < images.Length; i++)
{
- if (!image.IsLocalFile)
- {
- deletedImages.Add(image);
- continue;
- }
+ var image = images[i];
- try
- {
- _fileSystem.DeleteFile(image.Path);
- deleted = true;
- }
- catch (FileNotFoundException)
+ if (image.IsLocalFile)
{
+ try
+ {
+ _fileSystem.DeleteFile(image.Path);
+ }
+ catch (FileNotFoundException)
+ {
+ }
}
}
- item.RemoveImages(deletedImages);
-
- if (deleted)
- {
- item.ValidateImages(new DirectoryService(_fileSystem));
- }
+ item.RemoveImages(images);
}
- public bool MergeImages(BaseItem item, List<LocalImageInfo> images)
+ /// <summary>
+ /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to modify.</param>
+ /// <param name="images">The new images to place in <c>item</c>.</param>
+ /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
+ public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images)
{
- var changed = false;
+ var changed = item.ValidateImages(new DirectoryService(_fileSystem));
for (var i = 0; i < _singularImages.Length; i++)
{
@@ -398,18 +405,6 @@ namespace MediaBrowser.Providers.Manager
currentImage.DateModified = newDateModified;
}
}
- else
- {
- var existing = item.GetImageInfo(type, 0);
- if (existing != null)
- {
- if (existing.IsLocalFile && !File.Exists(existing.Path))
- {
- item.RemoveImage(existing);
- changed = true;
- }
- }
- }
}
if (UpdateMultiImages(item, images, ImageType.Backdrop))
@@ -417,15 +412,6 @@ namespace MediaBrowser.Providers.Manager
changed = true;
}
- var hasScreenshots = item as IHasScreenshots;
- if (hasScreenshots != null)
- {
- if (UpdateMultiImages(item, images, ImageType.Screenshot))
- {
- changed = true;
- }
- }
-
return changed;
}
@@ -444,12 +430,12 @@ namespace MediaBrowser.Providers.Manager
return null;
}
- private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type)
+ private bool UpdateMultiImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageType type)
{
var changed = false;
var newImageFileInfos = images
- .FindAll(i => i.Type == type)
+ .Where(i => i.Type == type)
.Select(i => i.FileInfo)
.ToList();
@@ -463,7 +449,6 @@ namespace MediaBrowser.Providers.Manager
private async Task<bool> DownloadImage(
BaseItem item,
- LibraryOptions libraryOptions,
IRemoteImageProvider provider,
RefreshResult result,
IEnumerable<RemoteImageInfo> images,
@@ -472,10 +457,10 @@ namespace MediaBrowser.Providers.Manager
CancellationToken cancellationToken)
{
var eligibleImages = images
- .Where(i => i.Type == type && !(i.Width.HasValue && i.Width.Value < minWidth))
+ .Where(i => i.Type == type && (i.Width == null || i.Width >= minWidth))
.ToList();
- if (EnableImageStub(item, libraryOptions) && eligibleImages.Count > 0)
+ if (EnableImageStub(item) && eligibleImages.Count > 0)
{
SaveImageStub(item, type, eligibleImages.Select(i => i.Url));
result.UpdateType |= ItemUpdateType.ImageUpdate;
@@ -489,7 +474,20 @@ namespace MediaBrowser.Providers.Manager
try
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
+
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
@@ -503,15 +501,8 @@ namespace MediaBrowser.Providers.Manager
result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad url's. Just move to the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
@@ -519,7 +510,7 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- private bool EnableImageStub(BaseItem item, LibraryOptions libraryOptions)
+ private bool EnableImageStub(BaseItem item)
{
if (item is LiveTvProgram)
{
@@ -531,7 +522,7 @@ namespace MediaBrowser.Providers.Manager
return true;
}
- if (item is IItemByName && !(item is MusicArtist))
+ if (item is IItemByName and not MusicArtist)
{
var hasDualAccess = item as IHasDualAccess;
if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
@@ -539,6 +530,7 @@ namespace MediaBrowser.Providers.Manager
return true;
}
}
+
// We always want to use prefetched images
return false;
}
@@ -563,7 +555,7 @@ namespace MediaBrowser.Providers.Manager
newIndex);
}
- private async Task DownloadBackdrops(BaseItem item, LibraryOptions libraryOptions, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
+ private async Task DownloadMultiImages(BaseItem item, ImageType imageType, ImageRefreshOptions refreshOptions, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
{
foreach (var image in images.Where(i => i.Type == imageType))
{
@@ -579,7 +571,7 @@ namespace MediaBrowser.Providers.Manager
var url = image.Url;
- if (EnableImageStub(item, libraryOptions))
+ if (EnableImageStub(item))
{
SaveImageStub(item, imageType, new[] { url });
result.UpdateType |= ItemUpdateType.ImageUpdate;
@@ -590,8 +582,21 @@ namespace MediaBrowser.Providers.Manager
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- // If there's already an image of the same size, skip it
- if (response.Content.Headers.ContentLength.HasValue)
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
+ // If there's already an image of the same file size, skip it unless doing a full refresh
+ if (response.Content.Headers.ContentLength.HasValue && !refreshOptions.IsReplacingImage(imageType))
{
try
{
@@ -617,15 +622,8 @@ namespace MediaBrowser.Providers.Manager
cancellationToken).ConfigureAwait(false);
result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad urls. Just move onto the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 827cb69b9..0af76f75a 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -1,8 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
@@ -505,6 +508,11 @@ namespace MediaBrowser.Providers.Manager
/// <summary>
/// Gets the providers.
/// </summary>
+ /// <param name="item">A media item.</param>
+ /// <param name="libraryOptions">The LibraryOptions to use.</param>
+ /// <param name="options">The MetadataRefreshOptions to use.</param>
+ /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
+ /// <param name="requiresRefresh">Specifies refresh mode.</param>
/// <returns>IEnumerable{`0}.</returns>
protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefreshOptions options, bool isFirstRefresh, bool requiresRefresh)
{
@@ -579,7 +587,7 @@ namespace MediaBrowser.Providers.Manager
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
{
// Get providers to refresh
- var providers = allImageProviders.Where(i => !(i is ILocalImageProvider));
+ var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
var dateLastImageRefresh = item.DateLastRefreshed;
@@ -617,7 +625,7 @@ namespace MediaBrowser.Providers.Manager
MetadataResult<TItemType> metadata,
TIdType id,
MetadataRefreshOptions options,
- List<IMetadataProvider> providers,
+ ICollection<IMetadataProvider> providers,
ItemImageProvider imageService,
CancellationToken cancellationToken)
{
@@ -672,8 +680,15 @@ namespace MediaBrowser.Providers.Manager
{
foreach (var remoteImage in localItem.RemoteImages)
{
- await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false);
- refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ try
+ {
+ await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false);
+ refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.type), remoteImage.url);
+ }
}
if (imageService.MergeImages(item, localItem.Images))
@@ -706,7 +721,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error in {provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider}", provider.Name);
// If a local provider fails, consider that a failure
refreshResult.ErrorMessage = ex.Message;
@@ -724,7 +739,7 @@ namespace MediaBrowser.Providers.Manager
refreshResult.Failures += remoteResult.Failures;
}
- if (providers.Any(i => !(i is ICustomMetadataProvider)))
+ if (providers.Any(i => i is not ICustomMetadataProvider))
{
if (refreshResult.UpdateType > ItemUpdateType.None)
{
@@ -743,7 +758,7 @@ namespace MediaBrowser.Providers.Manager
// var isUnidentified = failedProviderCount > 0 && successfulProviderCount == 0;
- foreach (var provider in customProviders.Where(i => !(i is IPreRefreshProvider)))
+ foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
{
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
}
@@ -778,7 +793,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
refreshResult.ErrorMessage = ex.Message;
- Logger.LogError(ex, "Error in {provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider}", provider.Name);
}
}
@@ -830,7 +845,7 @@ namespace MediaBrowser.Providers.Manager
{
refreshResult.Failures++;
refreshResult.ErrorMessage = ex.Message;
- Logger.LogError(ex, "Error in {provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider}", provider.Name);
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 2dfaa372c..0385ce6a7 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -9,7 +11,9 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -24,6 +28,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
@@ -209,8 +214,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(source));
}
- var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true);
-
+ var fileStream = AsyncFile.OpenRead(source);
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
}
@@ -235,14 +239,7 @@ namespace MediaBrowser.Providers.Manager
var preferredLanguage = item.GetPreferredMetadataLanguage();
- var languages = new List<string>();
- if (!query.IncludeAllLanguages && !string.IsNullOrWhiteSpace(preferredLanguage))
- {
- languages.Add(preferredLanguage);
- }
-
- // TODO include [query.IncludeAllLanguages] as an argument to the providers
- var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
+ var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -254,17 +251,21 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
/// <param name="item">The item.</param>
/// <param name="provider">The provider.</param>
- /// <param name="preferredLanguages">The preferred languages.</param>
+ /// <param name="preferredLanguage">The preferred language.</param>
+ /// <param name="includeAllLanguages">Whether to include all languages in results.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="type">The type.</param>
/// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
private async Task<IEnumerable<RemoteImageInfo>> GetImages(
BaseItem item,
IRemoteImageProvider provider,
- IReadOnlyCollection<string> preferredLanguages,
+ string preferredLanguage,
+ bool includeAllLanguages,
CancellationToken cancellationToken,
ImageType? type = null)
{
+ bool hasPreferredLanguage = !string.IsNullOrWhiteSpace(preferredLanguage);
+
try
{
var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
@@ -274,14 +275,17 @@ namespace MediaBrowser.Providers.Manager
result = result.Where(i => i.Type == type.Value);
}
- if (preferredLanguages.Count > 0)
+ if (!includeAllLanguages && hasPreferredLanguage)
{
- result = result.Where(i => string.IsNullOrEmpty(i.Language) ||
- preferredLanguages.Contains(i.Language, StringComparer.OrdinalIgnoreCase) ||
+ // Filter out languages that do not match the preferred languages.
+ //
+ // TODO: should exception case of "en" (English) eventually be removed?
+ result = result.Where(i => string.IsNullOrWhiteSpace(i.Language) ||
+ string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase) ||
string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
}
- return result;
+ return result.OrderByLanguageDescending(preferredLanguage);
}
catch (OperationCanceledException)
{
@@ -323,7 +327,7 @@ namespace MediaBrowser.Providers.Manager
.OrderBy(i =>
{
// See if there's a user-defined order
- if (!(i is ILocalImageProvider))
+ if (i is not ILocalImageProvider)
{
var fetcherOrder = typeFetcherOrder ?? currentOptions.ImageFetcherOrder;
var index = Array.IndexOf(fetcherOrder, i.Name);
@@ -390,7 +394,7 @@ namespace MediaBrowser.Providers.Manager
if (!includeDisabled)
{
// If locked only allow local providers
- if (item.IsLocked && !(provider is ILocalMetadataProvider) && !(provider is IForcedProvider))
+ if (item.IsLocked && provider is not ILocalMetadataProvider && provider is not IForcedProvider)
{
return false;
}
@@ -431,7 +435,7 @@ namespace MediaBrowser.Providers.Manager
if (!includeDisabled)
{
// If locked only allow local providers
- if (item.IsLocked && !(provider is ILocalImageProvider))
+ if (item.IsLocked && provider is not ILocalImageProvider)
{
if (refreshOptions.ImageRefreshMode != MetadataRefreshMode.FullRefresh)
{
@@ -466,7 +470,7 @@ namespace MediaBrowser.Providers.Manager
/// <returns>System.Int32.</returns>
private int GetOrder(IImageProvider provider)
{
- if (!(provider is IHasOrder hasOrder))
+ if (provider is not IHasOrder hasOrder)
{
return 0;
}
@@ -657,7 +661,7 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
{
- SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
+ SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
@@ -734,7 +738,7 @@ namespace MediaBrowser.Providers.Manager
{
if (libraryOptions.MetadataSavers == null)
{
- if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase))
+ if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -745,7 +749,7 @@ namespace MediaBrowser.Providers.Manager
{
// Manual edit occurred
// Even if save local is off, save locally anyway if the metadata file already exists
- if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item)))
+ if (saver is not IMetadataFileSaver fileSaver || !File.Exists(fileSaver.GetSavePath(item)))
{
return false;
}
@@ -760,7 +764,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase))
+ if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -1131,7 +1135,7 @@ namespace MediaBrowser.Providers.Manager
var albums = _libraryManager
.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
ArtistIds = new[] { item.Id },
DtoOptions = new DtoOptions(false)
{
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
index e5aa64b28..b90136d50 100644
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs
@@ -1,11 +1,13 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -135,7 +137,7 @@ namespace MediaBrowser.Providers.Manager
{
if (replaceData || !target.RunTimeTicks.HasValue)
{
- if (!(target is Audio) && !(target is Video))
+ if (target is not Audio && target is not Video)
{
target.RunTimeTicks = source.RunTimeTicks;
}
diff --git a/MediaBrowser.Providers/Manager/RefreshResult.cs b/MediaBrowser.Providers/Manager/RefreshResult.cs
index 72fc61e42..663ffc524 100644
--- a/MediaBrowser.Providers/Manager/RefreshResult.cs
+++ b/MediaBrowser.Providers/Manager/RefreshResult.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Providers.Manager
{
public ItemUpdateType UpdateType { get; set; }
- public string ErrorMessage { get; set; }
+ public string? ErrorMessage { get; set; }
public int Failures { get; set; }
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index cdb07a15d..dac5aaf56 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,9 +16,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
@@ -26,11 +26,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
- <AnalysisMode Condition=" '$(Configuration)' == 'Debug'">AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -48,5 +46,9 @@
<EmbeddedResource Include="Plugins\Omdb\Configuration\config.html" />
<None Remove="Plugins\MusicBrainz\Configuration\config.html" />
<EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" />
+ <None Remove="Plugins\StudioImages\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\StudioImages\Configuration\config.html" />
+ <None Remove="Plugins\Tmdb\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\Tmdb\Configuration\config.html" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index 03e45fb86..b4b1895f5 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -11,7 +11,9 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -19,38 +21,49 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
- /// Uses ffmpeg to create video images.
+ /// Uses <see cref="IMediaEncoder"/> to extract embedded images.
/// </summary>
public class AudioImageProvider : IDynamicImageProvider
{
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- public AudioImageProvider(IMediaEncoder mediaEncoder, IServerConfigurationManager config, IFileSystem fileSystem)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">The media source manager for fetching item streams.</param>
+ /// <param name="mediaEncoder">The media encoder for extracting embedded images.</param>
+ /// <param name="config">The server configuration manager for getting image paths.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public AudioImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerConfigurationManager config, IFileSystem fileSystem)
{
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_config = config;
_fileSystem = fileSystem;
}
- public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
+ private string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
+ /// <inheritdoc />
public string Name => "Image Extractor";
+ /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType> { ImageType.Primary };
+ return new[] { ImageType.Primary };
}
+ /// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
- var audio = (Audio)item;
-
- var imageStreams =
- audio.GetMediaStreams(MediaStreamType.EmbeddedImage)
- .Where(i => i.Type == MediaStreamType.EmbeddedImage)
- .ToList();
+ var imageStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = item.Id,
+ Type = MediaStreamType.EmbeddedImage
+ });
// Can't extract if we didn't find a video stream in the file
if (imageStreams.Count == 0)
@@ -61,7 +74,7 @@ namespace MediaBrowser.Providers.MediaInfo
return GetImage((Audio)item, imageStreams, cancellationToken);
}
- public async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken)
+ private async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken)
{
var path = GetAudioImagePath(item);
@@ -73,7 +86,7 @@ namespace MediaBrowser.Providers.MediaInfo
imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ??
imageStreams.FirstOrDefault();
- var imageStreamIndex = imageStream == null ? (int?)null : imageStream.Index;
+ var imageStreamIndex = imageStream?.Index;
var tempFile = await _mediaEncoder.ExtractAudioImage(item.Path, imageStreamIndex, cancellationToken).ConfigureAwait(false);
@@ -125,6 +138,7 @@ namespace MediaBrowser.Providers.MediaInfo
return Path.Join(AudioImagesPath, prefix, filename);
}
+ /// <inheritdoc />
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
new file mode 100644
index 000000000..425913501
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Resolves external audios for videos.
+ /// </summary>
+ public class AudioResolver
+ {
+ private readonly ILocalizationManager _localizationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly NamingOptions _namingOptions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioResolver"/> class.
+ /// </summary>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="namingOptions">The naming options.</param>
+ public AudioResolver(
+ ILocalizationManager localizationManager,
+ IMediaEncoder mediaEncoder,
+ NamingOptions namingOptions)
+ {
+ _localizationManager = localizationManager;
+ _mediaEncoder = mediaEncoder;
+ _namingOptions = namingOptions;
+ }
+
+ /// <summary>
+ /// Returns the audio streams found in the external audio files for the given video.
+ /// </summary>
+ /// <param name="video">The video to get the external audio streams from.</param>
+ /// <param name="startIndex">The stream index to start adding audio streams at.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>A list of external audio streams.</returns>
+ public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams(
+ Video video,
+ int startIndex,
+ IDirectoryService directoryService,
+ bool clearCache,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!video.IsFileProtocol)
+ {
+ yield break;
+ }
+
+ IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache);
+ foreach (string path in paths)
+ {
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
+ Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false);
+
+ foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
+ {
+ mediaStream.Index = startIndex++;
+ mediaStream.Type = MediaStreamType.Audio;
+ mediaStream.IsExternal = true;
+ mediaStream.Path = path;
+ mediaStream.IsDefault = false;
+ mediaStream.Title = null;
+
+ if (string.IsNullOrEmpty(mediaStream.Language))
+ {
+ // Try to translate to three character code
+ // Be flexible and check against both the full and three character versions
+ var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString();
+
+ if (language != fileNameWithoutExtension)
+ {
+ var culture = _localizationManager.FindLanguageInfo(language);
+
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+ mediaStream.Language = language;
+ }
+ }
+
+ yield return mediaStream;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns the external audio file paths for the given video.
+ /// </summary>
+ /// <param name="video">The video to get the external audio file paths from.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>A list of external audio file paths.</returns>
+ public IEnumerable<string> GetExternalAudioFiles(
+ Video video,
+ IDirectoryService directoryService,
+ bool clearCache)
+ {
+ if (!video.IsFileProtocol)
+ {
+ yield break;
+ }
+
+ // Check if video folder exists
+ string folder = video.ContainingFolderPath;
+ if (!Directory.Exists(folder))
+ {
+ yield break;
+ }
+
+ string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
+
+ var files = directoryService.GetFilePaths(folder, clearCache, true);
+ for (int i = 0; i < files.Count; i++)
+ {
+ string file = files[i];
+ if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase)
+ || !AudioFileParser.IsAudioFile(file, _namingOptions)
+ || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
+ // The audio filename must either be equal to the video filename or start with the video filename followed by a dot
+ if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)
+ || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
+ && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
+ && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)))
+ {
+ yield return file;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns the media info of the given audio file.
+ /// </summary>
+ /// <param name="path">The path to the audio file.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>The media info for the given audio file.</returns>
+ private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = DlnaProfileType.Audio,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = MediaProtocol.File
+ }
+ },
+ cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
new file mode 100644
index 000000000..96d7d139a
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -0,0 +1,248 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses <see cref="IMediaEncoder"/> to extract embedded images.
+ /// </summary>
+ public class EmbeddedImageProvider : IDynamicImageProvider, IHasOrder
+ {
+ private static readonly string[] _primaryImageFileNames =
+ {
+ "poster",
+ "folder",
+ "cover",
+ "default"
+ };
+
+ private static readonly string[] _backdropImageFileNames =
+ {
+ "backdrop",
+ "fanart",
+ "background",
+ "art"
+ };
+
+ private static readonly string[] _logoImageFileNames =
+ {
+ "logo",
+ };
+
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly ILogger<EmbeddedImageProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EmbeddedImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">The media source manager for fetching item streams and attachments.</param>
+ /// <param name="mediaEncoder">The media encoder for extracting attached/embedded images.</param>
+ /// <param name="logger">The logger.</param>
+ public EmbeddedImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ILogger<EmbeddedImageProvider> logger)
+ {
+ _mediaSourceManager = mediaSourceManager;
+ _mediaEncoder = mediaEncoder;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Embedded Image Extractor";
+
+ /// <inheritdoc />
+ // Default to after internet image providers but before Screen Grabber
+ public int Order => 99;
+
+ /// <inheritdoc />
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ if (item is Video)
+ {
+ if (item is Episode)
+ {
+ return new[]
+ {
+ ImageType.Primary,
+ };
+ }
+
+ return new[]
+ {
+ ImageType.Primary,
+ ImageType.Backdrop,
+ ImageType.Logo,
+ };
+ }
+
+ return Array.Empty<ImageType>();
+ }
+
+ /// <inheritdoc />
+ public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
+ {
+ var video = (Video)item;
+
+ // No support for these
+ if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
+ {
+ return Task.FromResult(new DynamicImageResponse { HasImage = false });
+ }
+
+ return GetEmbeddedImage(video, type, cancellationToken);
+ }
+
+ private async Task<DynamicImageResponse> GetEmbeddedImage(Video item, ImageType type, CancellationToken cancellationToken)
+ {
+ MediaSourceInfo mediaSource = new MediaSourceInfo
+ {
+ VideoType = item.VideoType,
+ IsoType = item.IsoType,
+ Protocol = item.PathProtocol ?? MediaProtocol.File,
+ };
+
+ string[] imageFileNames = type switch
+ {
+ ImageType.Primary => _primaryImageFileNames,
+ ImageType.Backdrop => _backdropImageFileNames,
+ ImageType.Logo => _logoImageFileNames,
+ _ => Array.Empty<string>()
+ };
+
+ if (imageFileNames.Length == 0)
+ {
+ _logger.LogWarning("Attempted to load unexpected image type: {Type}", type);
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ // Try attachments first
+ var attachmentStream = _mediaSourceManager.GetMediaAttachments(item.Id)
+ .FirstOrDefault(attachment => !string.IsNullOrEmpty(attachment.FileName)
+ && imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase)));
+
+ if (attachmentStream != null)
+ {
+ return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken);
+ }
+
+ // Fall back to EmbeddedImage streams
+ var imageStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = item.Id,
+ Type = MediaStreamType.EmbeddedImage
+ });
+
+ if (imageStreams.Count == 0)
+ {
+ // Can't extract if we don't have any EmbeddedImage streams
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ // Extract first stream containing an element of imageFileNames
+ var imageStream = imageStreams
+ .FirstOrDefault(stream => !string.IsNullOrEmpty(stream.Comment)
+ && imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase)));
+
+ // Primary type only: default to first image if none found by label
+ if (imageStream == null)
+ {
+ if (type == ImageType.Primary)
+ {
+ imageStream = imageStreams[0];
+ }
+ else
+ {
+ // No streams matched, abort
+ return new DynamicImageResponse { HasImage = false };
+ }
+ }
+
+ var format = imageStream.Codec switch
+ {
+ "mjpeg" => ImageFormat.Jpg,
+ "png" => ImageFormat.Png,
+ "gif" => ImageFormat.Gif,
+ _ => ImageFormat.Jpg
+ };
+
+ string extractedImagePath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, format, cancellationToken)
+ .ConfigureAwait(false);
+
+ return new DynamicImageResponse
+ {
+ Format = format,
+ HasImage = true,
+ Path = extractedImagePath,
+ Protocol = MediaProtocol.File
+ };
+ }
+
+ private async Task<DynamicImageResponse> ExtractAttachment(Video item, MediaAttachment attachmentStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
+ {
+ var extension = string.IsNullOrEmpty(attachmentStream.MimeType)
+ ? Path.GetExtension(attachmentStream.FileName)
+ : MimeTypes.ToExtension(attachmentStream.MimeType);
+
+ if (string.IsNullOrEmpty(extension))
+ {
+ extension = ".jpg";
+ }
+
+ ImageFormat format = extension switch
+ {
+ ".bmp" => ImageFormat.Bmp,
+ ".gif" => ImageFormat.Gif,
+ ".jpg" => ImageFormat.Jpg,
+ ".png" => ImageFormat.Png,
+ ".webp" => ImageFormat.Webp,
+ _ => ImageFormat.Jpg
+ };
+
+ string extractedAttachmentPath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, format, cancellationToken)
+ .ConfigureAwait(false);
+
+ return new DynamicImageResponse
+ {
+ Format = format,
+ HasImage = true,
+ Path = extractedAttachmentPath,
+ Protocol = MediaProtocol.File
+ };
+ }
+
+ /// <inheritdoc />
+ public bool Supports(BaseItem item)
+ {
+ if (item.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!item.IsFileProtocol)
+ {
+ return false;
+ }
+
+ return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index cf271e7db..9eb79c39d 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 4fff57273..19a435196 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -36,17 +39,10 @@ namespace MediaBrowser.Providers.MediaInfo
IHasItemChangeMonitor
{
private readonly ILogger<FFProbeProvider> _logger;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IItemRepository _itemRepo;
- private readonly IBlurayExaminer _blurayExaminer;
- private readonly ILocalizationManager _localization;
- private readonly IEncodingManager _encodingManager;
- private readonly IServerConfigurationManager _config;
- private readonly ISubtitleManager _subtitleManager;
- private readonly IChapterManager _chapterManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
private readonly SubtitleResolver _subtitleResolver;
+ private readonly AudioResolver _audioResolver;
+ private readonly FFProbeVideoInfo _videoProber;
+ private readonly FFProbeAudioInfo _audioProber;
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
@@ -61,21 +57,26 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ NamingOptions namingOptions)
{
_logger = logger;
- _mediaEncoder = mediaEncoder;
- _itemRepo = itemRepo;
- _blurayExaminer = blurayExaminer;
- _localization = localization;
- _encodingManager = encodingManager;
- _config = config;
- _subtitleManager = subtitleManager;
- _chapterManager = chapterManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
-
+ _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
_subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+ _videoProber = new FFProbeVideoInfo(
+ _logger,
+ mediaSourceManager,
+ mediaEncoder,
+ itemRepo,
+ blurayExaminer,
+ localization,
+ encodingManager,
+ config,
+ subtitleManager,
+ chapterManager,
+ libraryManager,
+ _audioResolver);
+ _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
}
public string Name => "ffprobe";
@@ -95,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo
var file = directoryService.GetFile(path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{
- _logger.LogDebug("Refreshing {0} due to date modified timestamp change.", path);
+ _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path);
return true;
}
}
@@ -105,7 +106,15 @@ namespace MediaBrowser.Providers.MediaInfo
&& !video.SubtitleFiles.SequenceEqual(
_subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
{
- _logger.LogDebug("Refreshing {0} due to external subtitles change.", item.Path);
+ _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
+ return true;
+ }
+
+ if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
+ && !video.AudioFiles.SequenceEqual(
+ _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
+ {
+ _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
}
@@ -175,20 +184,7 @@ namespace MediaBrowser.Providers.MediaInfo
FetchShortcutInfo(item);
}
- var prober = new FFProbeVideoInfo(
- _logger,
- _mediaSourceManager,
- _mediaEncoder,
- _itemRepo,
- _blurayExaminer,
- _localization,
- _encodingManager,
- _config,
- _subtitleManager,
- _chapterManager,
- _libraryManager);
-
- return prober.ProbeVideo(item, options, cancellationToken);
+ return _videoProber.ProbeVideo(item, options, cancellationToken);
}
private string NormalizeStrmLine(string line)
@@ -224,9 +220,7 @@ namespace MediaBrowser.Providers.MediaInfo
FetchShortcutInfo(item);
}
- var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _libraryManager);
-
- return prober.Probe(item, options, cancellationToken);
+ return _audioProber.Probe(item, options, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 12e1fbea5..77372e063 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1068, CS1591
using System;
using System.Collections.Generic;
@@ -42,6 +44,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ISubtitleManager _subtitleManager;
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
+ private readonly AudioResolver _audioResolver;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@@ -57,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ AudioResolver audioResolver)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
@@ -69,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleManager = subtitleManager;
_chapterManager = chapterManager;
_libraryManager = libraryManager;
+ _audioResolver = audioResolver;
_mediaSourceManager = mediaSourceManager;
}
@@ -212,6 +217,8 @@ namespace MediaBrowser.Providers.MediaInfo
await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+ await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (mediaInfo != null)
@@ -557,6 +564,7 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
+ true,
cancellationToken).ConfigureAwait(false);
// Rescan
@@ -572,6 +580,31 @@ namespace MediaBrowser.Providers.MediaInfo
}
/// <summary>
+ /// Adds the external audio.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="currentStreams">The current streams.</param>
+ /// <param name="options">The refreshOptions.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private async Task AddExternalAudioAsync(
+ Video video,
+ List<MediaStream> currentStreams,
+ MetadataRefreshOptions options,
+ CancellationToken cancellationToken)
+ {
+ var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
+ var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken);
+
+ await foreach (MediaStream externalAudioStream in externalAudioStreams)
+ {
+ currentStreams.Add(externalAudioStream);
+ }
+
+ // Select all external audio file paths
+ video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray();
+ }
+
+ /// <summary>
/// Creates dummy chapters.
/// </summary>
/// <param name="video">The video.</param>
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index 44ab5aa5b..b2b93940a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
@@ -36,6 +38,7 @@ namespace MediaBrowser.Providers.MediaInfo
IEnumerable<string> languages,
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
+ bool isAutomated,
CancellationToken cancellationToken)
{
var downloadedLanguages = new List<string>();
@@ -51,6 +54,7 @@ namespace MediaBrowser.Providers.MediaInfo
lang,
disabledSubtitleFetchers,
subtitleFetcherOrder,
+ isAutomated,
cancellationToken).ConfigureAwait(false);
if (downloaded)
@@ -71,6 +75,7 @@ namespace MediaBrowser.Providers.MediaInfo
string lang,
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
+ bool isAutomated,
CancellationToken cancellationToken)
{
if (video.VideoType != VideoType.VideoFile)
@@ -109,6 +114,7 @@ namespace MediaBrowser.Providers.MediaInfo
disabledSubtitleFetchers,
subtitleFetcherOrder,
mediaType,
+ isAutomated,
cancellationToken);
}
@@ -122,6 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
VideoContentType mediaType,
+ bool isAutomated,
CancellationToken cancellationToken)
{
// There's already subtitles for this language
@@ -169,7 +176,8 @@ namespace MediaBrowser.Providers.MediaInfo
IsPerfectMatch = requirePerfectMatch,
DisabledSubtitleFetchers = disabledSubtitleFetchers,
- SubtitleFetcherOrder = subtitleFetcherOrder
+ SubtitleFetcherOrder = subtitleFetcherOrder,
+ IsAutomated = isAutomated
};
if (video is Episode episode)
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index 3cd7ec772..ba284187e 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -10,15 +8,30 @@ using MediaBrowser.Model.Globalization;
namespace MediaBrowser.Providers.MediaInfo
{
+ /// <summary>
+ /// Resolves external subtitles for videos.
+ /// </summary>
public class SubtitleResolver
{
private readonly ILocalizationManager _localization;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleResolver"/> class.
+ /// </summary>
+ /// <param name="localization">The localization manager.</param>
public SubtitleResolver(ILocalizationManager localization)
{
_localization = localization;
}
+ /// <summary>
+ /// Retrieves the external subtitle streams for the provided video.
+ /// </summary>
+ /// <param name="video">The video to search from.</param>
+ /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>The external subtitle streams located.</returns>
public List<MediaStream> GetExternalSubtitleStreams(
Video video,
int startIndex,
@@ -54,6 +67,13 @@ namespace MediaBrowser.Providers.MediaInfo
return streams;
}
+ /// <summary>
+ /// Locates the external subtitle files for the provided video.
+ /// </summary>
+ /// <param name="video">The video to search from.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>The external subtitle file paths located.</returns>
public IEnumerable<string> GetExternalSubtitleFiles(
Video video,
IDirectoryService directoryService,
@@ -72,6 +92,13 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
+ /// <summary>
+ /// Extracts the subtitle files from the provided list and adds them to the list of streams.
+ /// </summary>
+ /// <param name="streams">The list of streams to add external subtitles to.</param>
+ /// <param name="videoPath">The path to the video file.</param>
+ /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
+ /// <param name="files">The files to add if they are subtitles.</param>
public void AddExternalSubtitleStreams(
List<MediaStream> streams,
string videoPath,
@@ -118,6 +145,12 @@ namespace MediaBrowser.Providers.MediaInfo
while (languageSpan.Length > 0)
{
var lastDot = languageSpan.LastIndexOf('.');
+ if (lastDot < videoFileNameWithoutExtension.Length)
+ {
+ languageSpan = ReadOnlySpan<char>.Empty;
+ break;
+ }
+
var currentSlice = languageSpan[lastDot..];
if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
|| currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
@@ -131,12 +164,19 @@ namespace MediaBrowser.Providers.MediaInfo
break;
}
- // Try to translate to three character code
- // Be flexible and check against both the full and three character versions
var language = languageSpan.ToString();
- var culture = _localization.FindLanguageInfo(language);
+ if (string.IsNullOrWhiteSpace(language))
+ {
+ language = null;
+ }
+ else
+ {
+ // Try to translate to three character code
+ // Be flexible and check against both the full and three character versions
+ var culture = _localization.FindLanguageInfo(language);
- language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+ }
mediaStream = new MediaStream
{
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 12b23098c..58651d42a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -64,7 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
var options = GetOptions();
- var types = new[] { "Episode", "Movie" };
+ var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie };
var dict = new Dictionary<Guid, BaseItem>();
@@ -194,6 +197,7 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
+ true,
cancellationToken).ConfigureAwait(false);
// Rescan
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index 30af6710a..d4bf62970 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -1,57 +1,63 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo
{
+ /// <summary>
+ /// Uses <see cref="IMediaEncoder"/> to create still images from the main video.
+ /// </summary>
public class VideoImageProvider : IDynamicImageProvider, IHasOrder
{
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger<VideoImageProvider> _logger;
- private readonly IFileSystem _fileSystem;
- public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger, IFileSystem fileSystem)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">The media source manager for fetching item streams.</param>
+ /// <param name="mediaEncoder">The media encoder for capturing images.</param>
+ /// <param name="logger">The logger.</param>
+ public VideoImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
{
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_logger = logger;
- _fileSystem = fileSystem;
}
+ /// <inheritdoc />
public string Name => "Screen Grabber";
+ /// <inheritdoc />
// Make sure this comes after internet image providers
public int Order => 100;
+ /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType> { ImageType.Primary };
+ return new[] { ImageType.Primary };
}
+ /// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
var video = (Video)item;
- // No support for this
- if (video.IsPlaceHolder)
- {
- return Task.FromResult(new DynamicImageResponse { HasImage = false });
- }
-
- // No support for this
- if (video.VideoType == VideoType.Dvd)
+ // No support for these
+ if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
@@ -59,80 +65,45 @@ namespace MediaBrowser.Providers.MediaInfo
// Can't extract if we didn't find a video stream in the file
if (!video.DefaultVideoStreamIndex.HasValue)
{
- _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {0}.", video.Path ?? string.Empty);
+ _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {Path}.", video.Path ?? string.Empty);
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
return GetVideoImage(video, cancellationToken);
}
- public async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
+ private async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
{
- var protocol = item.PathProtocol ?? MediaProtocol.File;
-
- var inputPath = item.Path;
-
- var mediaStreams =
- item.GetMediaStreams();
-
- var imageStreams =
- mediaStreams
- .Where(i => i.Type == MediaStreamType.EmbeddedImage)
- .ToList();
-
- var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("front", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault();
+ MediaSourceInfo mediaSource = new MediaSourceInfo
+ {
+ VideoType = item.VideoType,
+ IsoType = item.IsoType,
+ Protocol = item.PathProtocol ?? MediaProtocol.File,
+ };
- string extractedImagePath;
+ // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
+ // Always use 10 seconds for dvd because our duration could be out of whack
+ var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks > 0
+ ? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
+ : TimeSpan.FromSeconds(10);
- if (imageStream != null)
+ var query = new MediaStreamQuery { ItemId = item.Id, Index = item.DefaultVideoStreamIndex };
+ var videoStream = _mediaSourceManager.GetMediaStreams(query).FirstOrDefault();
+ if (videoStream == null)
{
- // Instead of using the raw stream index, we need to use nth video/embedded image stream
- var videoIndex = -1;
- foreach (var mediaStream in mediaStreams)
- {
- if (mediaStream.Type == MediaStreamType.Video ||
- mediaStream.Type == MediaStreamType.EmbeddedImage)
- {
- videoIndex++;
- }
-
- if (mediaStream == imageStream)
- {
- break;
- }
- }
-
- MediaSourceInfo mediaSource = new MediaSourceInfo
- {
- VideoType = item.VideoType,
- IsoType = item.IsoType,
- Protocol = item.PathProtocol.Value,
- };
-
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false);
+ query.Type = MediaStreamType.Video;
+ query.Index = null;
+ videoStream = _mediaSourceManager.GetMediaStreams(query).FirstOrDefault();
}
- else
+
+ if (videoStream == null)
{
- // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
- // Always use 10 seconds for dvd because our duration could be out of whack
- var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
- item.RunTimeTicks.Value > 0
- ? TimeSpan.FromTicks(Convert.ToInt64(item.RunTimeTicks.Value * .1))
- : TimeSpan.FromSeconds(10);
-
- var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
- var mediaSource = new MediaSourceInfo
- {
- VideoType = item.VideoType,
- IsoType = item.IsoType,
- Protocol = item.PathProtocol.Value,
- };
-
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("Skipping image extraction: no video stream found for {Path}.", item.Path ?? string.Empty);
+ return new DynamicImageResponse { HasImage = false };
}
+ string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+
return new DynamicImageResponse
{
Format = ImageFormat.Jpg,
@@ -142,6 +113,7 @@ namespace MediaBrowser.Providers.MediaInfo
};
}
+ /// <inheritdoc />
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
@@ -154,12 +126,7 @@ namespace MediaBrowser.Providers.MediaInfo
return false;
}
- if (item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia)
- {
- return true;
- }
-
- return false;
+ return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
}
}
}
diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
index a8d74aa0b..d00f37db5 100644
--- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/title/{0}";
+ public string? UrlFormatString => "https://www.imdb.com/title/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
index 8151ab471..1bb5e1ea8 100644
--- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/name/{0}";
+ public string? UrlFormatString => "https://www.imdb.com/name/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
diff --git a/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
index dddfd02e4..d3fce37c7 100644
--- a/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
+++ b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Providers.Music
{
public static class AlbumInfoExtensions
{
- public static string GetAlbumArtist(this AlbumInfo info)
+ public static string? GetAlbumArtist(this AlbumInfo info)
{
var id = info.SongInfos.SelectMany(i => i.AlbumArtists)
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Music
return info.AlbumArtists.Count > 0 ? info.AlbumArtists[0] : default;
}
- public static string GetReleaseGroupId(this AlbumInfo info)
+ public static string? GetReleaseGroupId(this AlbumInfo info)
{
var id = info.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
@@ -34,7 +34,7 @@ namespace MediaBrowser.Providers.Music
return id;
}
- public static string GetReleaseId(this AlbumInfo info)
+ public static string? GetReleaseId(this AlbumInfo info)
{
var id = info.GetProviderId(MetadataProvider.MusicBrainzAlbum);
@@ -47,9 +47,9 @@ namespace MediaBrowser.Providers.Music
return id;
}
- public static string GetMusicBrainzArtistId(this AlbumInfo info)
+ public static string? GetMusicBrainzArtistId(this AlbumInfo info)
{
- info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzAlbumArtist.ToString(), out string id);
+ info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzAlbumArtist.ToString(), out string? id);
if (string.IsNullOrEmpty(id))
{
@@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.Music
return id;
}
- public static string GetMusicBrainzArtistId(this ArtistInfo info)
+ public static string? GetMusicBrainzArtistId(this ArtistInfo info)
{
info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzArtist.ToString(), out var id);
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 8c9a1f59b..7c5b80e1e 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -81,7 +81,7 @@ namespace MediaBrowser.Providers.Music
if (!item.AlbumArtists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
{
item.AlbumArtists = artists;
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
return updateType;
@@ -100,7 +100,7 @@ namespace MediaBrowser.Providers.Music
if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
{
item.Artists = artists;
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
return updateType;
diff --git a/MediaBrowser.Providers/Music/ImvdbId.cs b/MediaBrowser.Providers/Music/ImvdbId.cs
index a1726b996..ed69f369c 100644
--- a/MediaBrowser.Providers/Music/ImvdbId.cs
+++ b/MediaBrowser.Providers/Music/ImvdbId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Music
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => null;
+ public string? UrlFormatString => null;
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index 067d585cb..fe9986d42 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -6,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -42,7 +45,7 @@ namespace MediaBrowser.Providers.Playlists
}
var extension = Path.GetExtension(path);
- if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(ItemUpdateType.None);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
index 138cfef19..3a400575b 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
index 85a28747f..ad0247fb2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -6,13 +8,14 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -57,7 +60,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index 25bb3f9ce..43f30824b 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591, SA1300
using System;
using System.Collections.Generic;
@@ -9,9 +11,9 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -30,7 +32,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IHttpClientFactory _httpClientFactory;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+#pragma warning disable SA1401, CA2211
public static AudioDbAlbumProvider Current;
+#pragma warning restore SA1401, CA2211
public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
@@ -64,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
@@ -170,8 +174,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ await using var xmlFileStream = new FileStream(path, fileStreamOptions);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
@@ -196,6 +203,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return Path.Combine(dataPath, "album.json");
}
+ /// <inheritdoc />
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+#pragma warning disable CA1034, CA2227
public class Album
{
public string idAlbum { get; set; }
@@ -279,11 +293,5 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
public List<Album> album { get; set; }
}
-
- /// <inheritdoc />
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
index 8aceb48c0..b9e57eb26 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index db8536cc9..9c2447660 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -6,13 +8,14 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -59,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index cbb61fa35..538dc67c4 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1034, CS1591, CA1002, SA1028, SA1300
using System;
using System.Collections.Generic;
@@ -8,9 +10,9 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -65,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
@@ -154,8 +156,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ await using var xmlFileStream = new FileStream(path, fileStreamOptions);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
@@ -183,6 +187,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return Path.Combine(dataPath, "artist.json");
}
+ /// <inheritdoc />
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
public class Artist
{
public string idArtist { get; set; }
@@ -268,15 +278,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public string strLocked { get; set; }
}
+#pragma warning disable CA2227
public class RootObject
{
public List<Artist> artists { get; set; }
}
-
- /// <inheritdoc />
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
index 014481da2..f8f6253ff 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
index 787539104..fd598c918 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs
index 664474dcd..d61ec6cb1 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index ba0d7b569..6c2ad0573 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
index 0cec9e359..9c27bd7d3 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
@@ -12,24 +12,13 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
public string Server
{
- get
- {
- return _server;
- }
-
- set
- {
- _server = value.TrimEnd('/');
- }
+ get => _server;
+ set => _server = value.TrimEnd('/');
}
public long RateLimit
{
- get
- {
- return _rateLimit;
- }
-
+ get => _rateLimit;
set
{
if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer)
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
deleted file mode 100644
index 5600c389c..000000000
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-
-namespace MediaBrowser.Providers.Music
-{
- public class MusicBrainzReleaseGroupExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-
- public class MusicBrainzAlbumArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
-
- public class MusicBrainzAlbumExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-
- public class MusicBrainzArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is MusicArtist;
- }
-
- public class MusicBrainzOtherArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
-
- public string Key => MetadataProvider.MusicBrainzArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-
- public class MusicBrainzTrackId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzTrack.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
new file mode 100644
index 000000000..c54cdda3d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzAlbumArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
new file mode 100644
index 000000000..8f7fadd06
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzAlbumExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 0023d5959..8a32cb07c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1401
using System;
using System.Collections.Generic;
@@ -12,7 +14,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
@@ -23,7 +24,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Music
{
- public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
+ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
{
/// <summary>
/// For each single MB lookup/search, this is the maximum number of
@@ -36,12 +37,11 @@ namespace MediaBrowser.Providers.Music
/// The Jellyfin user-agent is unrestricted but source IP must not exceed
/// one request per second, therefore we rate limit to avoid throttling.
/// Be prudent, use a value slightly above the minimun required.
- /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
+ /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting.
/// </summary>
private readonly long _musicBrainzQueryIntervalMs;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IApplicationHost _appHost;
private readonly ILogger<MusicBrainzAlbumProvider> _logger;
private readonly string _musicBrainzBaseUrl;
@@ -51,11 +51,9 @@ namespace MediaBrowser.Providers.Music
public MusicBrainzAlbumProvider(
IHttpClientFactory httpClientFactory,
- IApplicationHost appHost,
ILogger<MusicBrainzAlbumProvider> logger)
{
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
_logger = logger;
_musicBrainzBaseUrl = Plugin.Instance.Configuration.Server;
@@ -174,10 +172,10 @@ namespace MediaBrowser.Providers.Music
}
/// <inheritdoc />
- public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo id, CancellationToken cancellationToken)
+ public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
{
- var releaseId = id.GetReleaseId();
- var releaseGroupId = id.GetReleaseGroupId();
+ var releaseId = info.GetReleaseId();
+ var releaseGroupId = info.GetReleaseGroupId();
var result = new MetadataResult<MusicAlbum>
{
@@ -193,9 +191,9 @@ namespace MediaBrowser.Providers.Music
if (string.IsNullOrWhiteSpace(releaseId))
{
- var artistMusicBrainzId = id.GetMusicBrainzArtistId();
+ var artistMusicBrainzId = info.GetMusicBrainzArtistId();
- var releaseResult = await GetReleaseResult(artistMusicBrainzId, id.GetAlbumArtist(), id.Name, cancellationToken).ConfigureAwait(false);
+ var releaseResult = await GetReleaseResult(artistMusicBrainzId, info.GetAlbumArtist(), info.Name, cancellationToken).ConfigureAwait(false);
if (releaseResult != null)
{
@@ -305,181 +303,6 @@ namespace MediaBrowser.Providers.Music
return ReleaseResult.Parse(reader).FirstOrDefault();
}
- private class ReleaseResult
- {
- public string ReleaseId;
- public string ReleaseGroupId;
- public string Title;
- public string Overview;
- public int? Year;
-
- public List<ValueTuple<string, string>> Artists = new List<ValueTuple<string, string>>();
-
- public static IEnumerable<ReleaseResult> Parse(XmlReader reader)
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release-list":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using var subReader = reader.ReadSubtree();
- return ParseReleaseList(subReader).ToList();
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return Enumerable.Empty<ReleaseResult>();
- }
-
- private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader)
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- var releaseId = reader.GetAttribute("id");
-
- using var subReader = reader.ReadSubtree();
- var release = ParseRelease(subReader, releaseId);
- if (release != null)
- {
- yield return release;
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
- }
-
- private static ReleaseResult ParseRelease(XmlReader reader, string releaseId)
- {
- var result = new ReleaseResult
- {
- ReleaseId = releaseId
- };
-
- reader.MoveToContent();
- reader.Read();
-
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "title":
- {
- result.Title = reader.ReadElementContentAsString();
- break;
- }
-
- case "date":
- {
- var val = reader.ReadElementContentAsString();
- if (DateTime.TryParse(val, out var date))
- {
- result.Year = date.Year;
- }
-
- break;
- }
-
- case "annotation":
- {
- result.Overview = reader.ReadElementContentAsString();
- break;
- }
-
- case "release-group":
- {
- result.ReleaseGroupId = reader.GetAttribute("id");
- reader.Skip();
- break;
- }
-
- case "artist-credit":
- {
- using var subReader = reader.ReadSubtree();
- var artist = ParseArtistCredit(subReader);
-
- if (!string.IsNullOrEmpty(artist.Item1))
- {
- result.Artists.Add(artist);
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return result;
- }
- }
-
private static (string, string) ParseArtistCredit(XmlReader reader)
{
reader.MoveToContent();
@@ -496,15 +319,21 @@ namespace MediaBrowser.Providers.Music
{
case "name-credit":
{
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
using var subReader = reader.ReadSubtree();
return ParseArtistNameCredit(subReader);
}
default:
- {
- reader.Skip();
- break;
- }
+ {
+ reader.Skip();
+ break;
+ }
}
}
else
@@ -532,6 +361,12 @@ namespace MediaBrowser.Providers.Music
{
case "artist":
{
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
var id = reader.GetAttribute("id");
using var subReader = reader.ReadSubtree();
return ParseArtistArtistCredit(subReader, id);
@@ -634,8 +469,8 @@ namespace MediaBrowser.Providers.Music
};
using var reader = XmlReader.Create(oReader, settings);
- reader.MoveToContent();
- reader.Read();
+ await reader.MoveToContentAsync().ConfigureAwait(false);
+ await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -648,7 +483,7 @@ namespace MediaBrowser.Providers.Music
{
if (reader.IsEmptyElement)
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
continue;
}
@@ -658,14 +493,14 @@ namespace MediaBrowser.Providers.Music
default:
{
- reader.Skip();
+ await reader.SkipAsync().ConfigureAwait(false);
break;
}
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
@@ -711,6 +546,9 @@ namespace MediaBrowser.Providers.Music
/// A number of retries shall be made in order to try and satisfy the request before
/// giving up and returning null.
/// </summary>
+ /// <param name="url">Address of MusicBrainz server.</param>
+ /// <param name="cancellationToken">CancellationToken to use for method.</param>
+ /// <returns>Returns response from MusicBrainz service.</returns>
internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
{
await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -766,5 +604,201 @@ namespace MediaBrowser.Providers.Music
{
throw new NotImplementedException();
}
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _apiRequestLock?.Dispose();
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private class ReleaseResult
+ {
+ public string ReleaseId;
+ public string ReleaseGroupId;
+ public string Title;
+ public string Overview;
+ public int? Year;
+
+ public List<ValueTuple<string, string>> Artists = new List<ValueTuple<string, string>>();
+
+ public static IEnumerable<ReleaseResult> Parse(XmlReader reader)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "release-list":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ using var subReader = reader.ReadSubtree();
+ return ParseReleaseList(subReader).ToList();
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+
+ return Enumerable.Empty<ReleaseResult>();
+ }
+
+ private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "release":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ var releaseId = reader.GetAttribute("id");
+
+ using var subReader = reader.ReadSubtree();
+ var release = ParseRelease(subReader, releaseId);
+ if (release != null)
+ {
+ yield return release;
+ }
+
+ break;
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
+ private static ReleaseResult ParseRelease(XmlReader reader, string releaseId)
+ {
+ var result = new ReleaseResult
+ {
+ ReleaseId = releaseId
+ };
+
+ reader.MoveToContent();
+ reader.Read();
+
+ // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "title":
+ {
+ result.Title = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "date":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (DateTime.TryParse(val, out var date))
+ {
+ result.Year = date.Year;
+ }
+
+ break;
+ }
+
+ case "annotation":
+ {
+ result.Overview = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "release-group":
+ {
+ result.ReleaseGroupId = reader.GetAttribute("id");
+ reader.Skip();
+ break;
+ }
+
+ case "artist-credit":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
+ using var subReader = reader.ReadSubtree();
+ var artist = ParseArtistCredit(subReader);
+
+ if (!string.IsNullOrEmpty(artist.Item1))
+ {
+ result.Artists.Add(artist);
+ }
+
+ break;
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+
+ return result;
+ }
+ }
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
new file mode 100644
index 000000000..941ffea72
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is MusicArtist;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
index 2eab95294..1feb7f4ea 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,8 +13,8 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -22,6 +24,8 @@ namespace MediaBrowser.Providers.Music
{
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>
{
+ public string Name => "MusicBrainz";
+
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
{
@@ -53,7 +57,7 @@ namespace MediaBrowser.Providers.Music
}
}
- if (HasDiacritics(searchInfo.Name))
+ if (searchInfo.Name.HasDiacritics())
{
// Try again using the search with accent characters url
url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
@@ -215,18 +219,19 @@ namespace MediaBrowser.Providers.Music
return result;
}
- public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo id, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<MusicArtist>
{
Item = new MusicArtist()
};
- var musicBrainzId = id.GetMusicBrainzArtistId();
+ var musicBrainzId = info.GetMusicBrainzArtistId();
if (string.IsNullOrWhiteSpace(musicBrainzId))
{
- var searchResults = await GetSearchResults(id, cancellationToken).ConfigureAwait(false);
+ var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
var singleResult = searchResults.FirstOrDefault();
@@ -252,16 +257,6 @@ namespace MediaBrowser.Providers.Music
}
/// <summary>
- /// Determines whether the specified text has diacritics.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <returns><c>true</c> if the specified text has diacritics; otherwise, <c>false</c>.</returns>
- private bool HasDiacritics(string text)
- {
- return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
- }
-
- /// <summary>
/// Encodes an URL.
/// </summary>
/// <param name="name">The name.</param>
@@ -271,8 +266,6 @@ namespace MediaBrowser.Providers.Music
return WebUtility.UrlEncode(name);
}
- public string Name => "MusicBrainz";
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
new file mode 100644
index 000000000..05db2d98f
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzOtherArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
new file mode 100644
index 000000000..acb652fe0
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzReleaseGroupExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
new file mode 100644
index 000000000..14805b9b7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzTrackId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzTrack.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index 43bd3a472..cfa10dd64 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -11,6 +12,16 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
+ public const string DefaultServer = "https://musicbrainz.org";
+
+ public const long DefaultRateLimit = 2000u;
+
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
public static Plugin Instance { get; private set; }
public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
@@ -19,19 +30,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
public override string Description => "Get artist and album metadata from any MusicBrainz server.";
- public const string DefaultServer = "https://musicbrainz.org";
-
- public const long DefaultRateLimit = 2000u;
-
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
-
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs
index 196f14e7c..099547005 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
index 3d97a9de5..8bfdc461e 100644
--- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableInt32Converter.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace MediaBrowser.Providers.Plugins.Omdb
{
/// <summary>
/// Converts a string <c>N/A</c> to <c>string.Empty</c>.
@@ -16,7 +16,7 @@ namespace MediaBrowser.Common.Json.Converters
if (reader.TokenType == JsonTokenType.String)
{
var str = reader.GetString();
- if (str != null && str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ if (str == null || str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
{
return null;
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs
index 77cf46b70..f35880a04 100644
--- a/MediaBrowser.Common/Json/Converters/JsonOmdbNotAvailableStringConverter.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace MediaBrowser.Providers.Plugins.Omdb
{
/// <summary>
/// Converts a string <c>N/A</c> to <c>string.Empty</c>.
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index 24ef80a35..d8b33a799 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -17,24 +16,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- private readonly IHttpClientFactory _httpClientFactory;
private readonly OmdbItemProvider _itemProvider;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IApplicationHost _appHost;
+ private readonly OmdbProvider _omdbProvider;
public OmdbEpisodeProvider(
- IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
- _httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _appHost = appHost;
- _itemProvider = new OmdbItemProvider(_appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
+ _itemProvider = new OmdbItemProvider(httpClientFactory, libraryManager, fileSystem, configurationManager);
+ _omdbProvider = new OmdbProvider(httpClientFactory, fileSystem, configurationManager);
}
// After TheTvDb
@@ -44,12 +36,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
+ return _itemProvider.GetSearchResults(searchInfo, cancellationToken);
}
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Episode>()
+ var result = new MetadataResult<Episode>
{
Item = new Episode(),
QueriedById = true
@@ -61,13 +53,20 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return result;
}
- if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string seriesImdbId) && !string.IsNullOrEmpty(seriesImdbId))
+ if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
+ && !string.IsNullOrEmpty(seriesImdbId)
+ && info.IndexNumber.HasValue
+ && info.ParentIndexNumber.HasValue)
{
- if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
- {
- result.HasMetadata = await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager)
- .FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- }
+ result.HasMetadata = await _omdbProvider.FetchEpisodeData(
+ result,
+ info.IndexNumber.Value,
+ info.ParentIndexNumber.Value,
+ info.GetProviderId(MetadataProvider.Imdb),
+ seriesImdbId,
+ info.MetadataLanguage,
+ info.MetadataCountryCode,
+ cancellationToken).ConfigureAwait(false);
}
return result;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index df67aff31..60b373483 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -1,11 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
-using System.Globalization;
+using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -21,16 +22,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IApplicationHost _appHost;
+ private readonly OmdbProvider _omdbProvider;
- public OmdbImageProvider(IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbImageProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
_httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _appHost = appHost;
+ _omdbProvider = new OmdbProvider(_httpClientFactory, fileSystem, configurationManager);
}
public string Name => "The Open Movie Database";
@@ -50,38 +47,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var imdbId = item.GetProviderId(MetadataProvider.Imdb);
+ if (string.IsNullOrWhiteSpace(imdbId))
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- var list = new List<RemoteImageInfo>();
-
- var provider = new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager);
+ var rootObject = await _omdbProvider.GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
- if (!string.IsNullOrWhiteSpace(imdbId))
+ if (string.IsNullOrEmpty(rootObject.Poster))
{
- var rootObject = await provider.GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (!string.IsNullOrEmpty(rootObject.Poster))
+ // the poster url is sometimes higher quality than the poster api
+ return new[]
+ {
+ new RemoteImageInfo
{
- if (item is Episode)
- {
- // img.omdbapi.com is returning 404's
- list.Add(new RemoteImageInfo
- {
- ProviderName = Name,
- Url = rootObject.Poster
- });
- }
- else
- {
- list.Add(new RemoteImageInfo
- {
- ProviderName = Name,
- Url = string.Format(CultureInfo.InvariantCulture, "https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId)
- });
- }
+ ProviderName = Name,
+ Url = rootObject.Poster
}
- }
-
- return list;
+ };
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 428b0ded1..e5753b2b5 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1300
using System;
using System.Collections.Generic;
@@ -6,12 +8,11 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -30,13 +31,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IApplicationHost _appHost;
private readonly JsonSerializerOptions _jsonOptions;
+ private readonly OmdbProvider _omdbProvider;
public OmdbItemProvider(
- IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
@@ -44,9 +42,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _appHost = appHost;
+ _omdbProvider = new OmdbProvider(_httpClientFactory, fileSystem, configurationManager);
_jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
@@ -58,185 +54,166 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// After primary option
public int Order => 2;
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
+ }
+
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
- return GetSearchResults(searchInfo, "series", cancellationToken);
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
}
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
- return GetSearchResults(searchInfo, "movie", cancellationToken);
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
}
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo searchInfo, string type, CancellationToken cancellationToken)
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- return GetSearchResultsInternal(searchInfo, type, true, cancellationToken);
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
}
- private async Task<IEnumerable<RemoteSearchResult>> GetSearchResultsInternal(ItemLookupInfo searchInfo, string type, bool isSearch, CancellationToken cancellationToken)
+ private async Task<IEnumerable<RemoteSearchResult>> GetSearchResultsInternal(ItemLookupInfo searchInfo, bool isSearch, CancellationToken cancellationToken)
{
+ var type = searchInfo switch
+ {
+ EpisodeInfo => "episode",
+ SeriesInfo => "series",
+ _ => "movie"
+ };
+
+ // This is a bit hacky?
var episodeSearchInfo = searchInfo as EpisodeInfo;
+ var indexNumberEnd = episodeSearchInfo?.IndexNumberEnd;
var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
- var urlQuery = "plot=full&r=json";
- if (type == "episode" && episodeSearchInfo != null)
+ var urlQuery = new StringBuilder("plot=full&r=json");
+ if (episodeSearchInfo != null)
{
episodeSearchInfo.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out imdbId);
- }
-
- var name = searchInfo.Name;
- var year = searchInfo.Year;
+ if (searchInfo.IndexNumber.HasValue)
+ {
+ urlQuery.Append("&Episode=").Append(searchInfo.IndexNumber.Value);
+ }
- if (!string.IsNullOrWhiteSpace(name))
- {
- var parsedName = _libraryManager.ParseName(name);
- var yearInName = parsedName.Year;
- name = parsedName.Name;
- year ??= yearInName;
+ if (searchInfo.ParentIndexNumber.HasValue)
+ {
+ urlQuery.Append("&Season=").Append(searchInfo.ParentIndexNumber.Value);
+ }
}
if (string.IsNullOrWhiteSpace(imdbId))
{
- if (year.HasValue)
+ var name = searchInfo.Name;
+ var year = searchInfo.Year;
+ if (!string.IsNullOrWhiteSpace(name))
{
- urlQuery += "&y=" + year.Value.ToString(CultureInfo.InvariantCulture);
+ var parsedName = _libraryManager.ParseName(name);
+ var yearInName = parsedName.Year;
+ name = parsedName.Name;
+ year ??= yearInName;
}
- // &s means search and returns a list of results as opposed to t
- if (isSearch)
- {
- urlQuery += "&s=" + WebUtility.UrlEncode(name);
- }
- else
+ if (year.HasValue)
{
- urlQuery += "&t=" + WebUtility.UrlEncode(name);
+ urlQuery.Append("&y=").Append(year);
}
- urlQuery += "&type=" + type;
+ // &s means search and returns a list of results as opposed to t
+ urlQuery.Append(isSearch ? "&s=" : "&t=");
+ urlQuery.Append(WebUtility.UrlEncode(name));
+ urlQuery.Append("&type=")
+ .Append(type);
}
else
{
- urlQuery += "&i=" + imdbId;
+ urlQuery.Append("&i=")
+ .Append(imdbId);
isSearch = false;
}
- if (type == "episode")
- {
- if (searchInfo.IndexNumber.HasValue)
- {
- urlQuery += string.Format(CultureInfo.InvariantCulture, "&Episode={0}", searchInfo.IndexNumber);
- }
-
- if (searchInfo.ParentIndexNumber.HasValue)
- {
- urlQuery += string.Format(CultureInfo.InvariantCulture, "&Season={0}", searchInfo.ParentIndexNumber);
- }
- }
-
- var url = OmdbProvider.GetOmdbUrl(urlQuery);
+ var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString());
- using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var resultList = new List<SearchResult>();
if (isSearch)
{
var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (searchResultList != null && searchResultList.Search != null)
+ if (searchResultList?.Search != null)
{
- resultList.AddRange(searchResultList.Search);
+ var resultCount = searchResultList.Search.Count;
+ var result = new RemoteSearchResult[resultCount];
+ for (var i = 0; i < resultCount; i++)
+ {
+ result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd);
+ }
+
+ return result;
}
}
else
{
var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase))
{
- resultList.Add(result);
+ return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) };
}
}
- return resultList.Select(result =>
- {
- var item = new RemoteSearchResult
- {
- IndexNumber = searchInfo.IndexNumber,
- Name = result.Title,
- ParentIndexNumber = searchInfo.ParentIndexNumber,
- SearchProviderName = Name
- };
-
- if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue)
- {
- item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value;
- }
-
- item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
-
- if (result.Year.Length > 0
- && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
- {
- item.ProductionYear = parsedYear;
- }
-
- if (!string.IsNullOrEmpty(result.Released)
- && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
- {
- item.PremiereDate = released;
- }
-
- if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase))
- {
- item.ImageUrl = result.Poster;
- }
-
- return item;
- });
+ return Enumerable.Empty<RemoteSearchResult>();
}
public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
{
- return GetMovieResult<Trailer>(info, cancellationToken);
+ return GetResult<Trailer>(info, cancellationToken);
}
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
+ public Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
- return GetSearchResults(searchInfo, "movie", cancellationToken);
+ return GetResult<Series>(info, cancellationToken);
}
- public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
+ public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Series>
+ return GetResult<Movie>(info, cancellationToken);
+ }
+
+ private RemoteSearchResult ResultToMetadataResult(SearchResult result, ItemLookupInfo searchInfo, int? indexNumberEnd)
+ {
+ var item = new RemoteSearchResult
{
- Item = new Series(),
- QueriedById = true
+ IndexNumber = searchInfo.IndexNumber,
+ Name = result.Title,
+ ParentIndexNumber = searchInfo.ParentIndexNumber,
+ SearchProviderName = Name,
+ IndexNumberEnd = indexNumberEnd
};
- var imdbId = info.GetProviderId(MetadataProvider.Imdb);
- if (string.IsNullOrWhiteSpace(imdbId))
+ item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
+
+ if (OmdbProvider.TryParseYear(result.Year, out var parsedYear))
{
- imdbId = await GetSeriesImdbId(info, cancellationToken).ConfigureAwait(false);
- result.QueriedById = false;
+ item.ProductionYear = parsedYear;
}
- if (!string.IsNullOrEmpty(imdbId))
+ if (!string.IsNullOrEmpty(result.Released)
+ && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
{
- result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
- result.HasMetadata = true;
-
- await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ item.PremiereDate = released;
}
- return result;
- }
+ if (!string.IsNullOrWhiteSpace(result.Poster))
+ {
+ item.ImageUrl = result.Poster;
+ }
- public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
- {
- return GetMovieResult<Movie>(info, cancellationToken);
+ return item;
}
- private async Task<MetadataResult<T>> GetMovieResult<T>(ItemLookupInfo info, CancellationToken cancellationToken)
+ private async Task<MetadataResult<T>> GetResult<T>(ItemLookupInfo info, CancellationToken cancellationToken)
where T : BaseItem, new()
{
var result = new MetadataResult<T>
@@ -248,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var imdbId = info.GetProviderId(MetadataProvider.Imdb);
if (string.IsNullOrWhiteSpace(imdbId))
{
- imdbId = await GetMovieImdbId(info, cancellationToken).ConfigureAwait(false);
+ imdbId = await GetImdbId(info, cancellationToken).ConfigureAwait(false);
result.QueriedById = false;
}
@@ -257,22 +234,15 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await _omdbProvider.Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
}
- private async Task<string> GetMovieImdbId(ItemLookupInfo info, CancellationToken cancellationToken)
- {
- var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false);
- var first = results.FirstOrDefault();
- return first?.GetProviderId(MetadataProvider.Imdb);
- }
-
- private async Task<string> GetSeriesImdbId(SeriesInfo info, CancellationToken cancellationToken)
+ private async Task<string> GetImdbId(ItemLookupInfo info, CancellationToken cancellationToken)
{
- var results = await GetSearchResultsInternal(info, "series", false, cancellationToken).ConfigureAwait(false);
+ var results = await GetSearchResultsInternal(info, false, cancellationToken).ConfigureAwait(false);
var first = results.FirstOrDefault();
return first?.GetProviderId(MetadataProvider.Imdb);
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 46d303890..12ea2d55b 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -1,17 +1,19 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS159, SA1300
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -21,27 +23,38 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.Plugins.Omdb
{
+ /// <summary>Provider for OMDB service.</summary>
public class OmdbProvider
{
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- private readonly IApplicationHost _appHost;
private readonly JsonSerializerOptions _jsonOptions;
- public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+ /// <summary>Initializes a new instance of the <see cref="OmdbProvider"/> class.</summary>
+ /// <param name="httpClientFactory">HttpClientFactory to use for calls to OMDB service.</param>
+ /// <param name="fileSystem">IFileSystem to use for store OMDB data.</param>
+ /// <param name="configurationManager">IServerConfigurationManager to use.</param>
+ public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
- _appHost = appHost;
_jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
- _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
- _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
+ // These converters need to take priority
+ _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableStringConverter());
+ _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableInt32Converter());
}
+ /// <summary>Fetches data from OMDB service.</summary>
+ /// <param name="itemResult">Metadata about media item.</param>
+ /// <param name="imdbId">IMDB ID for media.</param>
+ /// <param name="language">Media language.</param>
+ /// <param name="country">Country of origin.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <typeparam name="T">The first generic type parameter.</typeparam>
+ /// <returns>Returns a Task object that can be awaited.</returns>
public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, CancellationToken cancellationToken)
where T : BaseItem
{
@@ -54,8 +67,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
+ var isEnglishRequested = IsConfiguredForEnglish(item, language);
// Only take the name and rating if the user's language is set to English, since Omdb has no localization
- if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
+ if (isEnglishRequested)
{
item.Name = result.Title;
@@ -65,9 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
- && year >= 0)
+ if (TryParseYear(result.Year, out var year))
{
item.ProductionYear = year;
}
@@ -80,14 +92,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.imdbVotes)
- && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
+ && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
&& voteCount >= 0)
{
// item.VoteCount = voteCount;
}
if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
+ && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -103,9 +115,20 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
}
- ParseAdditionalMetadata(itemResult, result);
+ ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
}
+ /// <summary>Gets data about an episode.</summary>
+ /// <param name="itemResult">Metadata about episode.</param>
+ /// <param name="episodeNumber">Episode number.</param>
+ /// <param name="seasonNumber">Season number.</param>
+ /// <param name="episodeImdbId">Episode ID.</param>
+ /// <param name="seriesImdbId">Season ID.</param>
+ /// <param name="language">Episode language.</param>
+ /// <param name="country">Country of origin.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <typeparam name="T">The first generic type parameter.</typeparam>
+ /// <returns>Whether operation was successful.</returns>
public async Task<bool> FetchEpisodeData<T>(MetadataResult<T> itemResult, int episodeNumber, int seasonNumber, string episodeImdbId, string seriesImdbId, string language, string country, CancellationToken cancellationToken)
where T : BaseItem
{
@@ -155,8 +178,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return false;
}
+ var isEnglishRequested = IsConfiguredForEnglish(item, language);
// Only take the name and rating if the user's language is set to English, since Omdb has no localization
- if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
+ if (isEnglishRequested)
{
item.Name = result.Title;
@@ -166,9 +190,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
- && year >= 0)
+ if (TryParseYear(result.Year, out var year))
{
item.ProductionYear = year;
}
@@ -181,14 +203,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.imdbVotes)
- && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
+ && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
&& voteCount >= 0)
{
// item.VoteCount = voteCount;
}
if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
+ && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -204,7 +226,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
}
- ParseAdditionalMetadata(itemResult, result);
+ ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
return true;
}
@@ -212,41 +234,54 @@ namespace MediaBrowser.Providers.Plugins.Omdb
internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
{
var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
- await using var stream = File.OpenRead(path);
- return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken);
+ await using var stream = AsyncFile.OpenRead(path);
+ return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
{
var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
- await using var stream = File.OpenRead(path);
- return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken);
+ await using var stream = AsyncFile.OpenRead(path);
+ return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
- internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
+ /// <summary>Gets OMDB URL.</summary>
+ /// <param name="query">Appends query string to URL.</param>
+ /// <returns>OMDB URL with optional query string.</returns>
+ public static string GetOmdbUrl(string query)
{
- if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id) && !string.IsNullOrEmpty(id))
+ const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
+
+ if (string.IsNullOrWhiteSpace(query))
{
- // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
- if (!string.IsNullOrWhiteSpace(id))
- {
- return true;
- }
+ return Url;
}
- return false;
+ return Url + "&" + query;
}
- public static string GetOmdbUrl(string query)
+ /// <summary>
+ /// Extract the year from a string.
+ /// </summary>
+ /// <param name="input">The input string.</param>
+ /// <param name="year">The year.</param>
+ /// <returns>A value indicating whether the input could successfully be parsed as a year.</returns>
+ public static bool TryParseYear(string input, [NotNullWhen(true)] out int? year)
{
- const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
+ if (string.IsNullOrEmpty(input))
+ {
+ year = 0;
+ return false;
+ }
- if (string.IsNullOrWhiteSpace(query))
+ if (int.TryParse(input.AsSpan(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
{
- return Url;
+ year = result;
+ return true;
}
- return Url + "&" + query;
+ year = 0;
+ return false;
}
private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
@@ -281,8 +316,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
"i={0}&plot=short&tomatoes=true&r=json",
imdbParam));
- var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<RootObject>(url, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
@@ -321,26 +356,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam,
seasonId));
- var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<SeasonRootObject>(url, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
}
- public async Task<T> GetDeserializedOmdbResponse<T>(HttpClient httpClient, string url, CancellationToken cancellationToken)
- {
- using var response = await GetOmdbResponse(httpClient, url, cancellationToken).ConfigureAwait(false);
- await using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- return await JsonSerializer.DeserializeAsync<T>(content, _jsonOptions, cancellationToken).ConfigureAwait(false);
- }
-
- public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken)
- {
- return httpClient.GetAsync(url, cancellationToken);
- }
-
internal string GetDataFilePath(string imdbId)
{
if (string.IsNullOrEmpty(imdbId))
@@ -369,31 +391,25 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return Path.Combine(dataPath, filename);
}
- private void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result)
+ private static void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result, bool isEnglishRequested)
where T : BaseItem
{
var item = itemResult.Item;
- var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport;
-
// Grab series genres because IMDb data is better than TVDB. Leave movies alone
// But only do it if English is the preferred language because this data will not be localized
- if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre))
+ if (isEnglishRequested && !string.IsNullOrWhiteSpace(result.Genre))
{
item.Genres = Array.Empty<string>();
- foreach (var genre in result.Genre
- .Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(i => i.Trim())
- .Where(i => !string.IsNullOrWhiteSpace(i)))
+ foreach (var genre in result.Genre.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
item.AddGenre(genre);
}
}
- if (isConfiguredForEnglish)
+ if (isEnglishRequested)
{
- // Omdb is currently English only, so for other languages skip this and let secondary providers fill it in
item.Overview = result.Plot;
}
@@ -406,7 +422,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Director.Trim(),
+ Name = result.Director,
Type = PersonType.Director
};
@@ -417,7 +433,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Writer.Trim(),
+ Name = result.Writer,
Type = PersonType.Writer
};
@@ -426,29 +442,34 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (!string.IsNullOrWhiteSpace(result.Actors))
{
- var actorList = result.Actors.Split(',');
+ var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var actor in actorList)
{
- if (!string.IsNullOrWhiteSpace(actor))
+ if (string.IsNullOrWhiteSpace(actor))
{
- var person = new PersonInfo
- {
- Name = actor.Trim(),
- Type = PersonType.Actor
- };
-
- itemResult.AddPerson(person);
+ continue;
}
+
+ var person = new PersonInfo
+ {
+ Name = actor,
+ Type = PersonType.Actor
+ };
+
+ itemResult.AddPerson(person);
}
}
}
- private bool IsConfiguredForEnglish(BaseItem item)
+ private static bool IsConfiguredForEnglish(BaseItem item, string language)
{
- var lang = item.GetPreferredMetadataLanguage();
+ if (string.IsNullOrEmpty(language))
+ {
+ language = item.GetPreferredMetadataLanguage();
+ }
// The data isn't localized and so can only be used for English users
- return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
+ return string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
}
internal class SeasonRootObject
@@ -525,7 +546,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (Ratings != null)
{
var rating = Ratings.FirstOrDefault(i => string.Equals(i.Source, "Rotten Tomatoes", StringComparison.OrdinalIgnoreCase));
- if (rating != null && rating.Value != null)
+ if (rating?.Value != null)
{
var value = rating.Value.TrimEnd('%');
if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var score))
@@ -539,10 +560,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
+#pragma warning disable CA1034
+ /// <summary>Describes OMDB rating.</summary>
public class OmdbRating
{
+ /// <summary>Gets or sets rating source.</summary>
public string Source { get; set; }
+ /// <summary>Gets or sets rating value.</summary>
public string Value { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
index d7f6781e5..a0fba48f0 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -11,6 +12,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
public static Plugin Instance { get; private set; }
public override Guid Id => new Guid("a628c0da-fac5-4c7e-9d1a-7134223f14c8");
@@ -22,12 +29,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
-
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs
new file mode 100644
index 000000000..fad989ab4
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs
@@ -0,0 +1,24 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.StudioImages
+{
+ public class PluginConfiguration : BasePluginConfiguration
+ {
+ private string _repository = Plugin.DefaultServer;
+
+ public string RepositoryUrl
+ {
+ get
+ {
+ return _repository;
+ }
+
+ set
+ {
+ _repository = value.TrimEnd('/');
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
new file mode 100644
index 000000000..f9fe3dc2e
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Studio Images</title>
+</head>
+<body>
+ <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div data-role="content">
+ <div class="content-primary">
+ <form class="configForm">
+ <div class="inputContainer">
+ <input is="emby-input" type="text" id="repository" required label="Repository" />
+ <div class="fieldDescription">This can be any Jellyfin-compatible artwork repository.</div>
+ </div>
+ <br />
+ <div>
+ <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+ </div>
+ </form>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var PluginConfig = {
+ pluginId: "872a7849-1171-458d-a6fb-3de3d442ad30"
+ };
+
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ var repository = document.querySelector('#repository');
+ repository.value = config.RepositoryUrl;
+ repository.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ Dashboard.hideLoadingMsg();
+ });
+ });
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.RepositoryUrl = document.querySelector('#server').value;
+
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
+ });
+ </script>
+ </div>
+</body>
+</html>
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
new file mode 100644
index 000000000..69a0569e5
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
@@ -0,0 +1,43 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.StudioImages
+{
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ {
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ public static Plugin Instance { get; private set; }
+
+ public override Guid Id => new Guid("872a7849-1171-458d-a6fb-3de3d442ad30");
+
+ public override string Name => "Studio Images";
+
+ public override string Description => "Get artwork for studios from any Jellyfin-compatible repository.";
+
+ // TODO change this for a Jellyfin-hosted repository.
+ public const string DefaultServer = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname";
+
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml";
+
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index f6153dd53..6fa34b985 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,7 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -16,6 +18,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.StudioImages;
namespace MediaBrowser.Providers.Studios
{
@@ -24,15 +27,17 @@ namespace MediaBrowser.Providers.Studios
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem;
+ private readonly String repositoryUrl;
public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
{
_config = config;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
+ repositoryUrl = Plugin.Instance.Configuration.RepositoryUrl;
}
- public string Name => "Emby Designs";
+ public string Name => "Artwork Repository";
public int Order => 0;
@@ -105,19 +110,19 @@ namespace MediaBrowser.Providers.Studios
private string GetUrl(string image, string filename)
{
- return string.Format(CultureInfo.InvariantCulture, "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.jpg", repositoryUrl, image, filename);
}
private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken)
{
- const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studiothumbs.txt";
+ string url = string.Format(CultureInfo.InvariantCulture, "{0}/studiothumbs.txt", repositoryUrl);
return EnsureList(url, file, _fileSystem, cancellationToken);
}
private Task<string> EnsurePosterList(string file, CancellationToken cancellationToken)
{
- const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studioposters.txt";
+ string url = string.Format(CultureInfo.InvariantCulture, "{0}/studioposters.txt", repositoryUrl);
return EnsureList(url, file, _fileSystem, cancellationToken);
}
@@ -146,7 +151,7 @@ namespace MediaBrowser.Providers.Studios
Directory.CreateDirectory(Path.GetDirectoryName(file));
await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(file, FileMode.Create);
+ await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
@@ -172,19 +177,16 @@ namespace MediaBrowser.Providers.Studios
public IEnumerable<string> GetAvailableImages(string file)
{
- using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var fileStream = File.OpenRead(file);
using var reader = new StreamReader(fileStream);
- var lines = new List<string>();
foreach (var line in reader.ReadAllLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
- lines.Add(line);
+ yield return line;
}
}
-
- return lines;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
new file mode 100644
index 000000000..0bab7c3ca
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
@@ -0,0 +1,41 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using TMDbLib.Objects.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Api
+{
+ /// <summary>
+ /// The TMDb api controller.
+ /// </summary>
+ [ApiController]
+ [Authorize(Policy = "DefaultAuthorization")]
+ [Route("[controller]")]
+ [Produces(MediaTypeNames.Application.Json)]
+ public class TmdbController : ControllerBase
+ {
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TmdbController"/> class.
+ /// </summary>
+ /// <param name="tmdbClientManager">The TMDb client manager.</param>
+ public TmdbController(TmdbClientManager tmdbClientManager)
+ {
+ _tmdbClientManager = tmdbClientManager;
+ }
+
+ /// <summary>
+ /// Gets the TMDb image configuration options.
+ /// </summary>
+ /// <returns>The image portion of the TMDb client configuration.</returns>
+ [HttpGet("ClientConfiguration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ConfigImageTypes> TmdbClientConfiguration()
+ {
+ return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
index 1f7ec6433..3217ac2f1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index 5ad61c567..29a557c31 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,9 +13,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
@@ -66,42 +66,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new List<RemoteImageInfo>();
-
- for (var i = 0; i < collection.Images.Posters.Count; i++)
- {
- var poster = collection.Images.Posters[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
- CommunityRating = poster.VoteAverage,
- VoteCount = poster.VoteCount,
- Width = poster.Width,
- Height = poster.Height,
- Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- });
- }
+ var posters = collection.Images.Posters;
+ var backdrops = collection.Images.Backdrops;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- for (var i = 0; i < collection.Images.Backdrops.Count; i++)
- {
- var backdrop = collection.Images.Backdrops[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
- CommunityRating = backdrop.VoteAverage,
- VoteCount = backdrop.VoteCount,
- Width = backdrop.Width,
- Height = backdrop.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- });
- }
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+ _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index ca1af6c49..62bc9c65f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -79,16 +81,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
return collections;
}
- public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
+ public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken)
{
- var tmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- var language = id.MetadataLanguage;
+ var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ var language = info.MetadataLanguage;
// We don't already have an Id, need to fetch it
if (tmdbId <= 0)
{
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
- var parsedName = _libraryManager.ParseName(id.Name);
+ var parsedName = _libraryManager.ParseName(info.Name);
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
new file mode 100644
index 000000000..dec796148
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -0,0 +1,50 @@
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Plugin configuration class for TMDb library.
+ /// </summary>
+ public class PluginConfiguration : BasePluginConfiguration
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether include adult content when searching with TMDb.
+ /// </summary>
+ public bool IncludeAdult { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether tags should be imported for series from TMDb.
+ /// </summary>
+ public bool ExcludeTagsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether tags should be imported for movies from TMDb.
+ /// </summary>
+ public bool ExcludeTagsMovies { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the maximum number of cast members to fetch for an item.
+ /// </summary>
+ public int MaxCastMembers { get; set; } = 15;
+
+ /// <summary>
+ /// Gets or sets a value indicating the poster image size to fetch.
+ /// </summary>
+ public string? PosterSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the backdrop image size to fetch.
+ /// </summary>
+ public string? BackdropSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the profile image size to fetch.
+ /// </summary>
+ public string? ProfileSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the still image size to fetch.
+ /// </summary>
+ public string? StillSize { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
new file mode 100644
index 000000000..52693795b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>TMDb</title>
+</head>
+<body>
+ <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div data-role="content">
+ <div class="content-primary">
+ <form class="configForm">
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="includeAdult" />
+ <span>Include adult content in search results.</span>
+ </label>
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="excludeTagsSeries" />
+ <span>Exclude tags/keywords from metadata fetched for series.</span>
+ </label>
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="excludeTagsMovies" />
+ <span>Exclude tags/keywords from metadata fetched for movies.</span>
+ </label>
+ <div class="inputContainer">
+ <input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
+ <div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
+ </div>
+ <div class="verticalSection verticalSection-extrabottompadding">
+ <h2>Image Scaling</h2>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectPosterSize" label="Poster"></select>
+ </div>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectBackdropSize" label="Backdrop"></select>
+ </div>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectProfileSize" label="Profile"></select>
+ </div>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectStillSize" label="Still"></select>
+ </div>
+ </div>
+ <div>
+ <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+ </div>
+ </form>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var PluginConfig = {
+ pluginId: "b8715ed1-6c47-4528-9ad3-f72deb539cd4"
+ };
+
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+
+ var clientConfig, pluginConfig;
+ var configureImageScaling = function() {
+ if (clientConfig === null || pluginConfig === null) {
+ return;
+ }
+
+ var sizeOptionsGenerator = function (size) {
+ return '<option value="' + size + '">' + size + '</option>';
+ }
+
+ var selPosterSize = document.querySelector('#selectPosterSize');
+ selPosterSize.innerHTML = clientConfig.PosterSizes.map(sizeOptionsGenerator);
+ selPosterSize.value = pluginConfig.PosterSize;
+
+ var selBackdropSize = document.querySelector('#selectBackdropSize');
+ selBackdropSize.innerHTML = clientConfig.BackdropSizes.map(sizeOptionsGenerator);
+ selBackdropSize.value = pluginConfig.BackdropSize;
+
+ var selProfileSize = document.querySelector('#selectProfileSize');
+ selProfileSize.innerHTML = clientConfig.ProfileSizes.map(sizeOptionsGenerator);
+ selProfileSize.value = pluginConfig.ProfileSize;
+
+ var selStillSize = document.querySelector('#selectStillSize');
+ selStillSize.innerHTML = clientConfig.StillSizes.map(sizeOptionsGenerator);
+ selStillSize.value = pluginConfig.StillSize;
+
+ Dashboard.hideLoadingMsg();
+ }
+
+ const request = {
+ url: ApiClient.getUrl('tmdb/ClientConfiguration'),
+ dataType: 'json',
+ type: 'GET',
+ headers: { accept: 'application/json' }
+ }
+ ApiClient.fetch(request).then(function (config) {
+ clientConfig = config;
+ configureImageScaling();
+ });
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#includeAdult').checked = config.IncludeAdult;
+ document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
+ document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
+
+ var maxCastMembers = document.querySelector('#maxCastMembers');
+ maxCastMembers.value = config.MaxCastMembers;
+ maxCastMembers.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ pluginConfig = config;
+ configureImageScaling();
+ });
+ });
+
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.IncludeAdult = document.querySelector('#includeAdult').checked;
+ config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked;
+ config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
+ config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
+ config.PosterSize = document.querySelector('#selectPosterSize').value;
+ config.BackdropSize = document.querySelector('#selectBackdropSize').value;
+ config.ProfileSize = document.querySelector('#selectProfileSize').value;
+ config.StillSize = document.querySelector('#selectStillSize').value;
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
+ });
+ </script>
+ </div>
+</body>
+</html>
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index f1a1b65d8..31310a8d4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index f34d689c1..f71f7bd10 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,9 +13,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find;
@@ -83,42 +83,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new List<RemoteImageInfo>();
-
- for (var i = 0; i < movie.Images.Posters.Count; i++)
- {
- var poster = movie.Images.Posters[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
- CommunityRating = poster.VoteAverage,
- VoteCount = poster.VoteCount,
- Width = poster.Width,
- Height = poster.Height,
- Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- });
- }
+ var posters = movie.Images.Posters;
+ var backdrops = movie.Images.Backdrops;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- for (var i = 0; i < movie.Images.Backdrops.Count; i++)
- {
- var backdrop = movie.Images.Backdrops[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(backdrop.FilePath),
- CommunityRating = backdrop.VoteAverage,
- VoteCount = backdrop.VoteCount,
- Width = backdrop.Width,
- Height = backdrop.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- });
- }
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+ _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 4a0884c07..e4a56fde9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -154,7 +157,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (movieResultFromImdbId?.MovieResults.Count > 0)
{
- tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString();
+ tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
@@ -239,8 +242,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieResult.Credits?.Cast != null)
{
- // TODO configurable
- foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
var personInfo = new PersonInfo
{
@@ -278,8 +280,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
index de74a7a4c..9804d60bd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index e4c908a62..7ce4cfe67 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -10,7 +10,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
@@ -61,23 +60,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count];
+ var profiles = personResult.Images.Profiles;
+ var remoteImages = new List<RemoteImageInfo>(profiles.Count);
- for (var i = 0; i < personResult.Images.Profiles.Count; i++)
- {
- var image = personResult.Images.Profiles[i];
- remoteImages[i] = new RemoteImageInfo
- {
- ProviderName = Name,
- Type = ImageType.Primary,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
- };
- }
+ _tmdbClientManager.ConvertProfilesToRemoteImageInfo(profiles, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 6db550b1d..8790e3759 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -77,14 +79,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return remoteSearchResults;
}
- public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken)
+ public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken)
{
- var personTmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ var personTmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
// We don't already have an Id, need to fetch it
if (personTmdbId <= 0)
{
- var personSearchResults = await _tmdbClientManager.SearchPersonAsync(id.Name, cancellationToken).ConfigureAwait(false);
+ var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResults.Count > 0)
{
personTmdbId = personSearchResults[0].Id;
@@ -95,7 +97,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId > 0)
{
- var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
result.HasMetadata = true;
@@ -103,7 +105,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
// Take name from incoming info, don't rename the person
// TODO: This should go in PersonMetadataService, not each person provider
- Name = id.Name,
+ Name = info.Name,
HomePageUrl = person.Homepage,
Overview = person.Biography,
PremiereDate = person.Birthday?.ToUniversalTime(),
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
new file mode 100644
index 000000000..4adde8366
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
@@ -0,0 +1,60 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Plugin class for the TMDb library.
+ /// </summary>
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Plugin"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">application paths.</param>
+ /// <param name="xmlSerializer">xml serializer.</param>
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ /// <summary>
+ /// Gets the instance of TMDb plugin.
+ /// </summary>
+ public static Plugin Instance { get; private set; }
+
+ /// <inheritdoc/>
+ public override Guid Id => new Guid("b8715ed1-6c47-4528-9ad3-f72deb539cd4");
+
+ /// <inheritdoc/>
+ public override string Name => "TMDb";
+
+ /// <inheritdoc/>
+ public override string Description => "Get metadata for movies and other video content from TheMovieDb.";
+
+ // TODO remove when plugin removed from server.
+
+ /// <inheritdoc/>
+ public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml";
+
+ /// <summary>
+ /// Return the plugin configuration page.
+ /// </summary>
+ /// <returns>PluginPageInfo.</returns>
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index ba18c542f..5eec776b5 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,9 +12,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -74,25 +74,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new RemoteImageInfo[stills.Count];
- for (var i = 0; i < stills.Count; i++)
- {
- var image = stills[i];
- remoteImages[i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetStillUrl(image.FilePath),
- CommunityRating = image.VoteAverage,
- VoteCount = image.VoteCount,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- };
- }
+ var remoteImages = new List<RemoteImageInfo>(stills.Count);
+
+ _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 8ec8f6464..f50f15877 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -152,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (credits?.Cast != null)
{
- foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
@@ -166,7 +169,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (credits?.GuestStars != null)
{
- foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
@@ -186,8 +189,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index 0d23c7872..4446fa966 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -11,9 +11,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -63,25 +61,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new RemoteImageInfo[posters.Count];
- for (var i = 0; i < posters.Count; i++)
- {
- var image = posters[i];
- remoteImages[i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(image.FilePath),
- CommunityRating = image.VoteAverage,
- VoteCount = image.VoteCount,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- };
- }
+ var remoteImages = new List<RemoteImageInfo>(posters.Count);
+
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 4c1f69763..27c52a5a2 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -33,7 +34,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
var result = new MetadataResult<Season>();
- info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId);
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? seriesTmdbId);
var seasonNumber = info.IndexNumber;
@@ -55,7 +56,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item = new Season
{
IndexNumber = seasonNumber,
- Overview = seasonResult?.Overview
+ Overview = seasonResult.Overview
};
if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
@@ -67,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var credits = seasonResult.Credits;
if (credits?.Cast != null)
{
- var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList();
+ var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
for (var i = 0; i < cast.Count; i++)
{
result.AddPerson(new PersonInfo
@@ -87,8 +88,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
index 6ecc055d7..8a2be80cd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 326c116b3..5ef3736c4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -11,9 +11,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -71,43 +69,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count];
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+ _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- for (var i = 0; i < posters.Count; i++)
- {
- var poster = posters[i];
- remoteImages[i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
- CommunityRating = poster.VoteAverage,
- VoteCount = poster.VoteCount,
- Width = poster.Width,
- Height = poster.Height,
- Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- };
- }
-
- for (var i = 0; i < backdrops.Count; i++)
- {
- var backdrop = series.Images.Backdrops[i];
- remoteImages[posters.Count + i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
- CommunityRating = backdrop.VoteAverage,
- VoteCount = backdrop.VoteCount,
- Width = backdrop.Width,
- Height = backdrop.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- };
- }
-
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index da76345b5..f565b6569 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -329,7 +332,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
if (seriesResult.Credits?.Cast != null)
{
- foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
var personInfo = new PersonInfo
{
@@ -363,8 +366,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 79ec6139d..28d6f4d0c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -1,8 +1,13 @@
-using System;
+#nullable disable
+
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Caching.Memory;
using TMDbLib.Client;
using TMDbLib.Objects.Collections;
@@ -18,7 +23,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <summary>
/// Manager class for abstracting the TMDb API client library.
/// </summary>
- public class TmdbClientManager
+ public class TmdbClientManager : IDisposable
{
private const int CacheDurationInHours = 1;
@@ -55,11 +60,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
+ var extraMethods = MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Videos;
+ if (!(Plugin.Instance?.Configuration.ExcludeTagsMovies).GetValueOrDefault())
+ {
+ extraMethods |= MovieMethods.Keywords;
+ }
+
movie = await _tmDbClient.GetMovieAsync(
tmdbId,
TmdbUtils.NormalizeLanguage(language),
imageLanguages,
- MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos,
+ extraMethods,
cancellationToken).ConfigureAwait(false);
if (movie != null)
@@ -121,11 +132,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
+ var extraMethods = TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups;
+ if (!(Plugin.Instance?.Configuration.ExcludeTagsSeries).GetValueOrDefault())
+ {
+ extraMethods |= TvShowMethods.Keywords;
+ }
+
series = await _tmDbClient.GetTvShowAsync(
tmdbId,
language: TmdbUtils.NormalizeLanguage(language),
includeImageLanguage: imageLanguages,
- extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups,
+ extraMethods: extraMethods,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (series != null)
@@ -242,7 +259,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
- var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken);
+ var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false);
if (group != null)
{
var season = group.Groups.Find(s => s.Order == seasonNumber);
@@ -358,7 +375,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -386,7 +403,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchPersonAsync(name, cancellationToken: cancellationToken)
+ .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -428,7 +445,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
+ .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -469,33 +486,29 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
- /// Gets the absolute URL of the poster.
+ /// Handles bad path checking and builds the absolute url.
/// </summary>
- /// <param name="posterPath">The relative URL of the poster.</param>
+ /// <param name="size">The image size to fetch.</param>
+ /// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns>
- public string GetPosterUrl(string posterPath)
+ private string GetUrl(string size, string path)
{
- if (string.IsNullOrEmpty(posterPath))
+ if (string.IsNullOrEmpty(path))
{
return null;
}
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
+ return _tmDbClient.GetImageUrl(size, path, true).ToString();
}
/// <summary>
- /// Gets the absolute URL of the backdrop image.
+ /// Gets the absolute URL of the poster.
/// </summary>
- /// <param name="posterPath">The relative URL of the backdrop image.</param>
+ /// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
- public string GetBackdropUrl(string posterPath)
+ public string GetPosterUrl(string posterPath)
{
- if (string.IsNullOrEmpty(posterPath))
- {
- return null;
- }
-
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
+ return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
/// <summary>
@@ -505,32 +518,150 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The absolute URL.</returns>
public string GetProfileUrl(string actorProfilePath)
{
- if (string.IsNullOrEmpty(actorProfilePath))
- {
- return null;
- }
+ return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
+ }
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
+ /// <summary>
+ /// Converts poster <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertPostersToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results);
}
/// <summary>
- /// Gets the absolute URL of the still image.
+ /// Converts backdrop <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
- /// <param name="filePath">The relative URL of the still image.</param>
- /// <returns>The absolute URL.</returns>
- public string GetStillUrl(string filePath)
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertBackdropsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results);
+ }
+
+ /// <summary>
+ /// Converts profile <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertProfilesToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results);
+ }
+
+ /// <summary>
+ /// Converts still <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertStillsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results);
+ }
+
+ /// <summary>
+ /// Converts <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="size">The size of the image to fetch.</param>
+ /// <param name="type">The type of the image.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ private void ConvertToRemoteImageInfo(List<ImageData> images, string size, ImageType type, string requestLanguage, List<RemoteImageInfo> results)
{
- if (string.IsNullOrEmpty(filePath))
+ // sizes provided are for original resolution, don't store them when downloading scaled images
+ var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase);
+
+ for (var i = 0; i < images.Count; i++)
{
- return null;
+ var image = images[i];
+
+ results.Add(new RemoteImageInfo
+ {
+ Url = GetUrl(size, image.FilePath),
+ CommunityRating = image.VoteAverage,
+ VoteCount = image.VoteCount,
+ Width = scaleImage ? null : image.Width,
+ Height = scaleImage ? null : image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage),
+ ProviderName = TmdbUtils.ProviderName,
+ Type = type,
+ RatingType = RatingType.Score
+ });
}
+ }
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
+ private async Task EnsureClientConfigAsync()
+ {
+ if (!_tmDbClient.HasConfig)
+ {
+ var config = await _tmDbClient.GetConfigAsync().ConfigureAwait(false);
+ ValidatePreferences(config);
+ }
}
- private Task EnsureClientConfigAsync()
+ private static void ValidatePreferences(TMDbConfig config)
{
- return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
+ var imageConfig = config.Images;
+
+ var pluginConfig = Plugin.Instance.Configuration;
+
+ if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
+ {
+ pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
+ }
+
+ if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
+ {
+ pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
+ }
+
+ if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
+ {
+ pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
+ }
+
+ if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
+ {
+ pluginConfig.StillSize = imageConfig.StillSizes[^1];
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="TMDbClient"/> configuration.
+ /// </summary>
+ /// <returns>The configuration.</returns>
+ public async Task<TMDbConfig> GetClientConfiguration()
+ {
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ return _tmDbClient.Config;
+ }
+
+ /// <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 (disposing)
+ {
+ _memoryCache?.Dispose();
+ _tmDbClient?.Dispose();
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index b713736a0..a3a78103e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@@ -31,11 +29,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
/// <summary>
- /// Maximum number of cast members to pull.
- /// </summary>
- public const int MaxCastMembers = 15;
-
- /// <summary>
/// The crew types to keep.
/// </summary>
public static readonly string[] WantedCrewTypes =
diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
index fe4749c79..bd301b5f0 100644
--- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
@@ -15,7 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
-[assembly: InternalsVisibleTo("Jellyfin.Common.Tests")]
+[assembly: InternalsVisibleTo("Jellyfin.Providers.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
index 78042b40d..091b33ce0 100644
--- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs
+++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
@@ -17,7 +17,8 @@ namespace MediaBrowser.Providers.Studios
IServerConfigurationManager serverConfigurationManager,
ILogger<StudioMetadataService> logger,
IProviderManager providerManager,
- IFileSystem fileSystem, ILibraryManager libraryManager)
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 6aacaa15d..34019e582 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -21,7 +24,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
-using static MediaBrowser.Model.IO.IODefaults;
namespace MediaBrowser.Providers.Subtitles
{
@@ -75,8 +77,7 @@ namespace MediaBrowser.Providers.Subtitles
var contentType = request.ContentType;
var providers = _subtitleProviders
- .Where(i => i.SupportedMediaTypes.Contains(contentType))
- .Where(i => !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparer.OrdinalIgnoreCase))
+ .Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))
.OrderBy(i =>
{
var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name);
@@ -187,8 +188,8 @@ namespace MediaBrowser.Providers.Subtitles
{
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
- using var stream = response.Stream;
- using var memoryStream = new MemoryStream();
+ await using var stream = response.Stream;
+ await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
@@ -236,7 +237,7 @@ namespace MediaBrowser.Providers.Subtitles
foreach (var savePath in savePaths)
{
- _logger.LogInformation("Saving subtitles to {0}", savePath);
+ _logger.LogInformation("Saving subtitles to {SavePath}", savePath);
_monitor.ReportFileSystemChangeBeginning(savePath);
@@ -244,15 +245,20 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true);
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.CreateNew;
+ fileOptions.PreallocationSize = stream.Length;
+ using var fs = new FileStream(savePath, fileOptions);
await stream.CopyToAsync(fs).ConfigureAwait(false);
return;
}
catch (Exception ex)
{
+// Bug in analyzer -- https://github.com/dotnet/roslyn-analyzers/issues/5160
+#pragma warning disable CA1508
(exs ??= new List<Exception>()).Add(ex);
+#pragma warning restore CA1508
}
finally
{
@@ -269,7 +275,7 @@ namespace MediaBrowser.Providers.Subtitles
}
/// <inheritdoc />
- public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken)
+ public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, bool isAutomated, CancellationToken cancellationToken)
{
if (video.VideoType != VideoType.VideoFile)
{
@@ -303,7 +309,8 @@ namespace MediaBrowser.Providers.Subtitles
ProductionYear = video.ProductionYear,
ProviderIds = video.ProviderIds,
RuntimeTicks = video.RunTimeTicks,
- IsPerfectMatch = isPerfectMatch ?? false
+ IsPerfectMatch = isPerfectMatch ?? false,
+ IsAutomated = isAutomated
};
if (video is Episode episode)
@@ -370,15 +377,15 @@ namespace MediaBrowser.Providers.Subtitles
}
/// <inheritdoc />
- public SubtitleProviderInfo[] GetSupportedProviders(BaseItem video)
+ public SubtitleProviderInfo[] GetSupportedProviders(BaseItem item)
{
VideoContentType mediaType;
- if (video is Episode)
+ if (item is Episode)
{
mediaType = VideoContentType.Episode;
}
- else if (video is Movie)
+ else if (item is Movie)
{
mediaType = VideoContentType.Movie;
}
diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
index 170f1bdd8..08cb6ced9 100644
--- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
+++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
@@ -25,46 +25,46 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType)
{
- var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
var seriesName = item.FindSeriesName();
if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
{
item.SeriesName = seriesName;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seasonName = item.FindSeasonName();
if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal))
{
item.SeasonName = seasonName;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesId = item.FindSeriesId();
if (!item.SeriesId.Equals(seriesId))
{
item.SeriesId = seriesId;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seasonId = item.FindSeasonId();
if (!item.SeasonId.Equals(seasonId))
{
item.SeasonId = seasonId;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
{
item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- return updateType;
+ return updatedType;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 4e59f78bc..b173fc7a3 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -31,9 +31,9 @@ namespace MediaBrowser.Providers.TV
protected override bool EnableUpdatingPremiereDateFromChildren => true;
/// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType)
{
- var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
if (item.IndexNumber.HasValue && item.IndexNumber.Value == 0)
{
@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.TV
if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase))
{
item.Name = seasonZeroDisplayName;
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updatedType |= ItemUpdateType.MetadataEdit;
}
}
@@ -50,24 +50,24 @@ namespace MediaBrowser.Providers.TV
if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
{
item.SeriesName = seriesName;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
{
item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesId = item.FindSeriesId();
if (!item.SeriesId.Equals(seriesId))
{
item.SeriesId = seriesId;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- return updateType;
+ return updatedType;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 967908197..770dc3e00 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -128,11 +130,12 @@ namespace MediaBrowser.Providers.TV
/// <returns>The async task.</returns>
private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
{
- var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
- .Cast<Episode>()
+ var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
+ var episodesInSeriesFolder = seriesChildren
+ .OfType<Episode>()
.Where(i => !i.IsInSeasonFolder);
- List<Season> seasons = series.Children.OfType<Season>().ToList();
+ List<Season> seasons = seriesChildren.OfType<Season>().ToList();
// Loop through the unique season numbers
foreach (var episode in episodesInSeriesFolder)
@@ -187,7 +190,7 @@ namespace MediaBrowser.Providers.TV
SeriesName = series.Name
};
- series.AddChild(season, cancellationToken);
+ series.AddChild(season);
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
index 3cb18e424..087e4036a 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.TV
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
+ public string? UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Series;
diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs
index 8325bfdbd..c73989e76 100644
--- a/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs
+++ b/MediaBrowser.XbmcMetadata/Configuration/NfoConfigurationFactory.cs
@@ -15,8 +15,8 @@ namespace MediaBrowser.XbmcMetadata.Configuration
{
new ConfigurationStore
{
- ConfigurationType = typeof(XbmcMetadataOptions),
- Key = "xbmcmetadata"
+ ConfigurationType = typeof(XbmcMetadataOptions),
+ Key = "xbmcmetadata"
}
};
}
diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs
index d02aea556..935ff5f59 100644
--- a/MediaBrowser.XbmcMetadata/EntryPoint.cs
+++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs
@@ -71,7 +71,7 @@ namespace MediaBrowser.XbmcMetadata
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error saving metadata for {path}", item.Path ?? item.Name);
+ _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name);
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index 2904b40ec..926be5a92 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,13 +15,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 302c93f0b..bcf9a8366 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
@@ -57,8 +58,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
_directoryService = directoryService;
}
- protected CultureInfo UsCulture { get; } = new CultureInfo("en-US");
-
/// <summary>
/// Gets the logger.
/// </summary>
@@ -148,80 +147,76 @@ namespace MediaBrowser.XbmcMetadata.Parsers
return;
}
- using (var fileStream = File.OpenRead(metadataFile))
- using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
- {
- item.ResetPeople();
+ item.ResetPeople();
- // Need to handle a url after the xml data
- // http://kodi.wiki/view/NFO_files/movies
+ // Need to handle a url after the xml data
+ // http://kodi.wiki/view/NFO_files/movies
- var xml = streamReader.ReadToEnd();
+ var xml = File.ReadAllText(metadataFile);
- // Find last closing Tag
- // Need to do this in two steps to account for random > characters after the closing xml
- var index = xml.LastIndexOf(@"</", StringComparison.Ordinal);
+ // Find last closing Tag
+ // Need to do this in two steps to account for random > characters after the closing xml
+ var index = xml.LastIndexOf(@"</", StringComparison.Ordinal);
- // If closing tag exists, move to end of Tag
- if (index != -1)
- {
- index = xml.IndexOf('>', index);
- }
-
- if (index != -1)
- {
- var endingXml = xml.Substring(index);
+ // If closing tag exists, move to end of Tag
+ if (index != -1)
+ {
+ index = xml.IndexOf('>', index);
+ }
- ParseProviderLinks(item.Item, endingXml);
+ if (index != -1)
+ {
+ var endingXml = xml.AsSpan().Slice(index);
- // If the file is just an imdb url, don't go any further
- if (index == 0)
- {
- return;
- }
+ ParseProviderLinks(item.Item, endingXml);
- xml = xml.Substring(0, index + 1);
- }
- else
+ // If the file is just an imdb url, don't go any further
+ if (index == 0)
{
- // If the file is just provider urls, handle that
- ParseProviderLinks(item.Item, xml);
-
return;
}
- // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
- try
+ xml = xml.Substring(0, index + 1);
+ }
+ else
+ {
+ // If the file is just provider urls, handle that
+ ParseProviderLinks(item.Item, xml);
+
+ return;
+ }
+
+ // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
+ try
+ {
+ using (var stringReader = new StringReader(xml))
+ using (var reader = XmlReader.Create(stringReader, settings))
{
- using (var stringReader = new StringReader(xml))
- using (var reader = XmlReader.Create(stringReader, settings))
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- reader.MoveToContent();
- reader.Read();
+ cancellationToken.ThrowIfCancellationRequested();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ if (reader.NodeType == XmlNodeType.Element)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- else
- {
- reader.Read();
- }
+ FetchDataFromXmlNode(reader, item);
+ }
+ else
+ {
+ reader.Read();
}
}
}
- catch (XmlException)
- {
- }
+ }
+ catch (XmlException)
+ {
}
}
- protected void ParseProviderLinks(T item, string xml)
+ protected void ParseProviderLinks(T item, ReadOnlySpan<char> xml)
{
if (ProviderIdParsers.TryFindImdbId(xml, out var imdbId))
{
@@ -271,9 +266,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- item.DateCreated = added.ToUniversalTime();
+ item.DateCreated = added;
}
else
{
@@ -312,7 +307,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrEmpty(text))
{
- if (float.TryParse(text, NumberStyles.Any, UsCulture, out var value))
+ if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
item.CriticRating = value;
}
@@ -373,7 +368,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && userData != null)
{
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var count))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count))
{
userData.PlayCount = count;
}
@@ -387,9 +382,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && userData != null)
{
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
{
- userData.LastPlayedDate = added.ToUniversalTime();
+ userData.LastPlayedDate = added;
}
else
{
@@ -478,7 +473,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(text))
{
- if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, UsCulture, out var runtime))
+ if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
@@ -688,9 +683,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.PremiereDate = date.ToUniversalTime();
+ item.PremiereDate = date;
item.ProductionYear = date.Year;
}
}
@@ -706,9 +701,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
{
- item.EndDate = date.ToUniversalTime();
+ item.EndDate = date;
}
}
@@ -783,59 +778,25 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "thumb":
{
- var artType = reader.GetAttribute("aspect");
- var val = reader.ReadElementContentAsString();
+ FetchThumbNode(reader, itemResult);
+ break;
+ }
- // skip:
- // - empty aspect tag
- // - empty uri
- // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies
- if (string.IsNullOrEmpty(artType) || string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal))
+ case "fanart":
+ {
+ if (reader.IsEmptyElement)
{
+ reader.Read();
break;
}
- ImageType imageType = GetImageType(artType);
-
- if (!Uri.TryCreate(val, UriKind.Absolute, out var uri))
+ using var subtree = reader.ReadSubtree();
+ if (!subtree.ReadToDescendant("thumb"))
{
- Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, item.Name);
break;
}
- if (uri.IsFile)
- {
- // only allow one item of each type
- if (itemResult.Images.Any(x => x.Type == imageType))
- {
- break;
- }
-
- var fileSystemMetadata = _directoryService.GetFile(val);
- // non existing file returns null
- if (fileSystemMetadata == null || !fileSystemMetadata.Exists)
- {
- Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, item.Name);
- break;
- }
-
- itemResult.Images.Add(new LocalImageInfo()
- {
- FileInfo = fileSystemMetadata,
- Type = imageType
- });
- }
- else
- {
- // only allow one item of each type
- if (itemResult.RemoteImages.Any(x => x.type == imageType))
- {
- break;
- }
-
- itemResult.RemoteImages.Add((uri.ToString(), imageType));
- }
-
+ FetchThumbNode(subtree, itemResult);
break;
}
@@ -858,6 +819,68 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
+ private void FetchThumbNode(XmlReader reader, MetadataResult<T> itemResult)
+ {
+ var artType = reader.GetAttribute("aspect");
+ var val = reader.ReadElementContentAsString();
+
+ // artType is null if the thumb node is a child of the fanart tag
+ // -> set image type to fanart
+ if (string.IsNullOrWhiteSpace(artType))
+ {
+ artType = "fanart";
+ }
+
+ // skip:
+ // - empty uri
+ // - tag containing '.' because we can't set images for seasons, episodes or movie sets within series or movies
+ if (string.IsNullOrEmpty(val) || artType.Contains('.', StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ ImageType imageType = GetImageType(artType);
+
+ if (!Uri.TryCreate(val, UriKind.Absolute, out var uri))
+ {
+ Logger.LogError("Image location {Path} specified in nfo file for {ItemName} is not a valid URL or file path.", val, itemResult.Item.Name);
+ return;
+ }
+
+ if (uri.IsFile)
+ {
+ // only allow one item of each type
+ if (itemResult.Images.Any(x => x.Type == imageType))
+ {
+ return;
+ }
+
+ var fileSystemMetadata = _directoryService.GetFile(val);
+ // non existing file returns null
+ if (fileSystemMetadata == null || !fileSystemMetadata.Exists)
+ {
+ Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, itemResult.Item.Name);
+ return;
+ }
+
+ itemResult.Images.Add(new LocalImageInfo()
+ {
+ FileInfo = fileSystemMetadata,
+ Type = imageType
+ });
+ }
+ else
+ {
+ // only allow one item of each type
+ if (itemResult.RemoteImages.Any(x => x.type == imageType))
+ {
+ return;
+ }
+
+ itemResult.RemoteImages.Add((uri.ToString(), imageType));
+ }
+ }
+
private void FetchFromFileInfoNode(XmlReader reader, T item)
{
reader.MoveToContent();
@@ -1205,7 +1228,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name)
{
case "name":
- name = reader.ReadElementContentAsString() ?? string.Empty;
+ name = reader.ReadElementContentAsString();
break;
case "role":
@@ -1250,7 +1273,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var intVal))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 6b1607530..d2f349ad7 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.IO;
-using System.Text;
using System.Threading;
using System.Xml;
using MediaBrowser.Common.Configuration;
@@ -40,72 +39,68 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <inheritdoc />
protected override void Fetch(MetadataResult<Episode> item, string metadataFile, XmlReaderSettings settings, CancellationToken cancellationToken)
{
- using (var fileStream = File.OpenRead(metadataFile))
- using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
- {
- item.ResetPeople();
+ item.ResetPeople();
- var xmlFile = streamReader.ReadToEnd();
+ var xmlFile = File.ReadAllText(metadataFile);
- var srch = "</episodedetails>";
- var index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
+ var srch = "</episodedetails>";
+ var index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
- var xml = xmlFile;
+ var xml = xmlFile;
- if (index != -1)
- {
- xml = xmlFile.Substring(0, index + srch.Length);
- xmlFile = xmlFile.Substring(index + srch.Length);
- }
+ if (index != -1)
+ {
+ xml = xmlFile.Substring(0, index + srch.Length);
+ xmlFile = xmlFile.Substring(index + srch.Length);
+ }
- // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
- try
+ // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions
+ try
+ {
+ // Extract episode details from the first episodedetails block
+ using (var stringReader = new StringReader(xml))
+ using (var reader = XmlReader.Create(stringReader, settings))
{
- // Extract episode details from the first episodedetails block
- using (var stringReader = new StringReader(xml))
- using (var reader = XmlReader.Create(stringReader, settings))
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- reader.MoveToContent();
- reader.Read();
+ cancellationToken.ThrowIfCancellationRequested();
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ if (reader.NodeType == XmlNodeType.Element)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- else
- {
- reader.Read();
- }
+ FetchDataFromXmlNode(reader, item);
+ }
+ else
+ {
+ reader.Read();
}
}
+ }
- // Extract the last episode number from nfo
- // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
- while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
+ // Extract the last episode number from nfo
+ // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
+ while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
+ {
+ xml = xmlFile.Substring(0, index + srch.Length);
+ xmlFile = xmlFile.Substring(index + srch.Length);
+
+ using (var stringReader = new StringReader(xml))
+ using (var reader = XmlReader.Create(stringReader, settings))
{
- xml = xmlFile.Substring(0, index + srch.Length);
- xmlFile = xmlFile.Substring(index + srch.Length);
+ reader.MoveToContent();
- using (var stringReader = new StringReader(xml))
- using (var reader = XmlReader.Create(stringReader, settings))
+ if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
{
- reader.MoveToContent();
-
- if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
- {
- item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
- }
+ item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
}
}
}
- catch (XmlException)
- {
- }
+ }
+ catch (XmlException)
+ {
}
}
@@ -168,7 +163,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
}
@@ -184,7 +179,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsAfterSeasonNumber = rval;
}
@@ -200,7 +195,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
}
@@ -216,7 +211,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
}
@@ -232,7 +227,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var rval))
+ if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index 2c893ac9f..3011d65a6 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -103,7 +103,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
else
{
- Logger.LogInformation("Unrecognized series status: " + status);
+ Logger.LogInformation("Unrecognized series status: {Status}", status);
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 3be35e2d9..d09981304 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -203,8 +204,15 @@ namespace MediaBrowser.XbmcMetadata.Savers
// On Windows, saving the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
+ var fileStreamOptions = new FileStreamOptions()
+ {
+ Mode = FileMode.Create,
+ Access = FileAccess.Write,
+ Share = FileShare.None,
+ PreallocationSize = stream.Length
+ };
+
+ using (var filestream = new FileStream(path, fileStreamOptions))
{
stream.CopyTo(filestream);
}
@@ -555,7 +563,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
// Series xml saver already saves this
- if (!(item is Series))
+ if (item is not Series)
{
var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrEmpty(tvdb))
@@ -582,7 +590,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("countrycode", item.PreferredMetadataCountryCode);
}
- if (item.PremiereDate.HasValue && !(item is Episode))
+ if (item.PremiereDate.HasValue && item is not Episode)
{
var formatString = options.ReleaseDateFormat;
@@ -605,7 +613,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (item.EndDate.HasValue)
{
- if (!(item is Episode))
+ if (item is not Episode)
{
var formatString = options.ReleaseDateFormat;
@@ -996,7 +1004,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
var name = reader.Name;
if (!_commonTags.Contains(name)
- && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase))
+ && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase))
{
writer.WriteNode(reader, false);
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
index 62f80e81b..2cd3fdf02 100644
--- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
@@ -17,8 +17,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
/// </summary>
public class EpisodeNfoSaver : BaseNfoSaver
{
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeNfoSaver"/> class.
/// </summary>
@@ -60,17 +58,17 @@ namespace MediaBrowser.XbmcMetadata.Savers
if (episode.IndexNumber.HasValue)
{
- writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.IndexNumberEnd.HasValue)
{
- writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(_usCulture));
+ writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.ParentIndexNumber.HasValue)
{
- writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.PremiereDate.HasValue)
@@ -84,28 +82,28 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
if (episode.AirsAfterSeasonNumber.HasValue && episode.AirsAfterSeasonNumber.Value != -1)
{
- writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1)
{
- writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.AirsBeforeSeasonNumber.HasValue && episode.AirsBeforeSeasonNumber.Value != -1)
{
- writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
}
if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1)
{
- writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture));
+ writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(CultureInfo.InvariantCulture));
}
var specialSeason = episode.AiredSeasonNumber;
if (specialSeason.HasValue && specialSeason.Value != -1)
{
- writer.WriteElementString("displayseason", specialSeason.Value.ToString(_usCulture));
+ writer.WriteElementString("displayseason", specialSeason.Value.ToString(CultureInfo.InvariantCulture));
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 412e8031b..21e7e2335 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -82,7 +82,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
// Check parent for null to avoid running this against things like video backdrops
- if (item is Video video && !(item is Episode) && !video.ExtraType.HasValue)
+ if (item is Video video && item is not Episode && !video.ExtraType.HasValue)
{
return updateType >= MinimumUpdateType;
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
index b9d73ba82..e97550630 100644
--- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs
@@ -52,7 +52,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
return false;
}
- if (!(item is Season))
+ if (item is not Season)
{
return false;
}
diff --git a/README.md b/README.md
index 6859a8a76..6653bff11 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,9 @@
<a href="https://github.com/jellyfin/jellyfin/commits/master.atom">
<img alt="Master Commits RSS Feed"" src="https://img.shields.io/badge/rss-commits-ffa500?logo=rss" />
</a>
+<a href="https://lgtm.com/projects/g/jellyfin/jellyfin/alerts/">
+<img alt="Total LGTM alerts" src="https://img.shields.io/lgtm/alerts/g/jellyfin/jellyfin.svg?logo=lgtm&logoWidth=18"/>
+</a>
</p>
---
@@ -68,6 +71,8 @@ Check out our <a href="https://translate.jellyfin.org">Weblate instance</a> to h
<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-web/multi-auto.svg" alt="Detailed Translation Status"/>
</a>
+---
+
## Jellyfin Server
This repository contains the code for Jellyfin's backend server. Note that this is only one of many projects under the Jellyfin GitHub [organization](https://github.com/jellyfin/) on GitHub. If you want to contribute, you can start by checking out our [documentation](https://jellyfin.org/docs/general/contributing/index.html) to see what to work on.
@@ -78,7 +83,7 @@ These instructions will help you get set up with a local development environment
### Prerequisites
-Before the project can be built, you must first install the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) on your system.
+Before the project can be built, you must first install the [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download).
@@ -162,3 +167,13 @@ switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.
**NOTE:** The setup wizard can not be run if the web client is hosted separately.
+
+---
+<p align="center">
+This project is supported by:
+<br/>
+<br/>
+<a href="https://www.digitalocean.com"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50px" alt="DigitalOcean"></a>
+ &nbsp;
+<a href="https://www.jetbrains.com"><img src="https://gist.githubusercontent.com/anthonylavado/e8b2403deee9581e0b4cb8cd675af7db/raw/fa104b7d73f759d7262794b94569f1b89df41c0b/jetbrains.svg" height="50px" alt="JetBrains logo"></a>
+</p>
diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs
index 7d6a471f9..5d7da4124 100644
--- a/RSSDP/DisposableManagedObjectBase.cs
+++ b/RSSDP/DisposableManagedObjectBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Text;
namespace Rssdp.Infrastructure
@@ -45,11 +46,11 @@ namespace Rssdp.Infrastructure
const string ArgFormat = "{0}: {1}\r\n";
- builder.AppendFormat("{0}\r\n", header);
+ builder.AppendFormat(CultureInfo.InvariantCulture, "{0}\r\n", header);
foreach (var pair in values)
{
- builder.AppendFormat(ArgFormat, pair.Key, pair.Value);
+ builder.AppendFormat(CultureInfo.InvariantCulture, ArgFormat, pair.Key, pair.Value);
}
builder.Append("\r\n");
diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs
index c56249523..6b6c13d99 100644
--- a/RSSDP/HttpParserBase.cs
+++ b/RSSDP/HttpParserBase.cs
@@ -82,7 +82,7 @@ namespace Rssdp.Infrastructure
throw new ArgumentNullException(nameof(versionData));
}
- var versionSeparatorIndex = versionData.IndexOf('/');
+ var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal);
if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length)
{
throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData));
@@ -101,7 +101,7 @@ namespace Rssdp.Infrastructure
{
// Header format is
// name: value
- var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase);
+ var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal);
var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();
var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();
@@ -172,7 +172,7 @@ namespace Rssdp.Infrastructure
else
{
var segments = headerValue.Split(SeparatorCharacters);
- if (headerValue.Contains('"'))
+ if (headerValue.Contains('"', StringComparison.Ordinal))
{
for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
{
diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs
index 4114195a6..a3e100796 100644
--- a/RSSDP/HttpRequestParser.cs
+++ b/RSSDP/HttpRequestParser.cs
@@ -1,6 +1,6 @@
using System;
-using System.Linq;
using System.Net.Http;
+using Jellyfin.Extensions;
namespace Rssdp.Infrastructure
{
@@ -86,7 +86,7 @@ namespace Rssdp.Infrastructure
/// <param name="headerName">A string containing the name of the header to return the type of.</param>
protected override bool IsContentHeader(string headerName)
{
- return ContentHeaderNames.Contains(headerName, StringComparer.OrdinalIgnoreCase);
+ return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs
index 0dd4bb45a..3e361465d 100644
--- a/RSSDP/HttpResponseParser.cs
+++ b/RSSDP/HttpResponseParser.cs
@@ -1,7 +1,7 @@
using System;
-using System.Linq;
using System.Net;
using System.Net.Http;
+using Jellyfin.Extensions;
namespace Rssdp.Infrastructure
{
@@ -49,7 +49,7 @@ namespace Rssdp.Infrastructure
/// <returns>A boolean, true if th specified header relates to HTTP content, otherwise false.</returns>
protected override bool IsContentHeader(string headerName)
{
- return ContentHeaderNames.Contains(headerName, StringComparer.OrdinalIgnoreCase);
+ return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index c64ee9389..77130983b 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -11,9 +11,11 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <AnalysisMode>AllDisabledByDefault</AnalysisMode>
+ <Nullable>disable</Nullable>
+ <NoWarn>CA2016</NoWarn>
</PropertyGroup>
</Project>
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index e49c0e77b..58dabc628 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -396,7 +395,7 @@ namespace Rssdp.Infrastructure
// Strange cannot convert compiler error here if I don't explicitly
// assign or cast to Action first. Assignment is easier to read,
// so went with that.
- ProcessMessage(System.Text.UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress);
+ ProcessMessage(UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress);
}
}
catch (ObjectDisposedException)
diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs
index 4005d836d..c826830f1 100644
--- a/RSSDP/SsdpDevice.cs
+++ b/RSSDP/SsdpDevice.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Globalization;
using Rssdp.Infrastructure;
namespace Rssdp
@@ -134,11 +135,13 @@ namespace Rssdp
{
get
{
- return String.Format("urn:{0}:{3}:{1}:{2}",
- this.DeviceTypeNamespace ?? String.Empty,
- this.DeviceType ?? String.Empty,
- this.DeviceVersion,
- this.DeviceClass ?? "device");
+ return String.Format(
+ CultureInfo.InvariantCulture,
+ "urn:{0}:{3}:{1}:{2}",
+ this.DeviceTypeNamespace ?? String.Empty,
+ this.DeviceType ?? String.Empty,
+ this.DeviceVersion,
+ this.DeviceClass ?? "device");
}
}
diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs
index 188e298e2..3a52b2a3e 100644
--- a/RSSDP/SsdpDeviceLocator.cs
+++ b/RSSDP/SsdpDeviceLocator.cs
@@ -513,7 +513,7 @@ namespace Rssdp.Infrastructure
return TimeSpan.Zero;
}
- return (TimeSpan)(headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero);
+ return headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero;
}
private void RemoveExpiredDevicesFromCache()
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index c9e795d56..a7767b3c0 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
@@ -14,8 +15,6 @@ namespace Rssdp.Infrastructure
/// </summary>
public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher
{
- private readonly INetworkManager _networkManager;
-
private ISsdpCommunicationsServer _CommsServer;
private string _OSName;
private string _OSVersion;
@@ -37,19 +36,17 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Default constructor.
/// </summary>
- public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, INetworkManager networkManager,
- string osName, string osVersion, bool sendOnlyMatchedHost)
+ public SsdpDevicePublisher(
+ ISsdpCommunicationsServer communicationsServer,
+ string osName,
+ string osVersion,
+ bool sendOnlyMatchedHost)
{
if (communicationsServer == null)
{
throw new ArgumentNullException(nameof(communicationsServer));
}
- if (networkManager == null)
- {
- throw new ArgumentNullException(nameof(networkManager));
- }
-
if (osName == null)
{
throw new ArgumentNullException(nameof(osName));
@@ -76,7 +73,6 @@ namespace Rssdp.Infrastructure
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
_Random = new Random();
- _networkManager = networkManager;
_CommsServer = communicationsServer;
_CommsServer.RequestReceived += CommsServer_RequestReceived;
_OSName = osName;
@@ -233,7 +229,7 @@ namespace Rssdp.Infrastructure
{
if (String.IsNullOrEmpty(searchTarget))
{
- WriteTrace(String.Format("Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
+ WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString()));
return;
}
@@ -340,7 +336,7 @@ namespace Rssdp.Infrastructure
private string GetUsn(string udn, string fullDeviceType)
{
- return String.Format("{0}::{1}", udn, fullDeviceType);
+ return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType);
}
private async void SendSearchResponse(
@@ -363,7 +359,7 @@ namespace Rssdp.Infrastructure
values["DATE"] = DateTime.UtcNow.ToString("r");
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
values["ST"] = searchTarget;
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["USN"] = uniqueServiceName;
values["LOCATION"] = rootDevice.Location.ToString();
@@ -497,7 +493,7 @@ namespace Rssdp.Infrastructure
values["DATE"] = DateTime.UtcNow.ToString("r");
values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds;
values["LOCATION"] = rootDevice.Location.ToString();
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["NTS"] = "ssdp:alive";
values["NT"] = notificationType;
values["USN"] = uniqueServiceName;
@@ -522,7 +518,7 @@ namespace Rssdp.Infrastructure
}
tasks.Add(SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken));
- tasks.Add(SendByeByeNotification(device, String.Format("urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
+ tasks.Add(SendByeByeNotification(device, String.Format(CultureInfo.InvariantCulture, "urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken));
foreach (var childDevice in device.Devices)
{
@@ -542,7 +538,7 @@ namespace Rssdp.Infrastructure
// If needed later for non-server devices, these headers will need to be dynamic
values["HOST"] = "239.255.255.250:1900";
values["DATE"] = DateTime.UtcNow.ToString("r");
- values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
+ values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion);
values["NTS"] = "ssdp:byebye";
values["NT"] = notificationType;
values["USN"] = uniqueServiceName;
@@ -550,7 +546,7 @@ namespace Rssdp.Infrastructure
var message = BuildMessage(header, values);
var sendCount = IsDisposed ? 1 : 3;
- WriteTrace(String.Format("Sent byebye notification"), device);
+ WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device);
return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken);
}
diff --git a/bump_version b/bump_version
index 8012ec07f..41d27f5c8 100755
--- a/bump_version
+++ b/bump_version
@@ -21,7 +21,14 @@ fi
shared_version_file="./SharedVersion.cs"
build_file="./build.yaml"
# csproj files for nuget packages
-jellyfin_subprojects=( MediaBrowser.Common/MediaBrowser.Common.csproj Jellyfin.Data/Jellyfin.Data.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj MediaBrowser.Model/MediaBrowser.Model.csproj Emby.Naming/Emby.Naming.csproj )
+jellyfin_subprojects=(
+ MediaBrowser.Common/MediaBrowser.Common.csproj
+ Jellyfin.Data/Jellyfin.Data.csproj
+ MediaBrowser.Controller/MediaBrowser.Controller.csproj
+ MediaBrowser.Model/MediaBrowser.Model.csproj
+ Emby.Naming/Emby.Naming.csproj
+ src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+)
new_version="$1"
@@ -45,7 +52,8 @@ echo $old_version
# Set the build.yaml version to the specified new_version
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
-sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
+new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
+sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
# update nuget package version
for subproject in ${jellyfin_subprojects[@]}; do
@@ -57,26 +65,29 @@ for subproject in ${jellyfin_subprojects[@]}; do
| sed -E 's/<VersionPrefix>([0-9\.]+[-a-z0-9]*)<\/VersionPrefix>/\1/'
)"
echo old nuget version: $old_version
+ new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
# Set the nuget version to the specified new_version
- sed -i "s|${old_version}|${new_version}|g" ${subproject}
+ sed -i "s|${old_version}|${new_version_sed}|g" ${subproject}
done
if [[ ${new_version} == *"-"* ]]; then
- new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
+ new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
+ new_version_deb_sup=""
else
- new_version_deb="${new_version}-1"
+ new_version_pkg="${new_version}"
+ new_version_deb_sup="-1"
fi
# Update the metapackage equivs file
debian_equivs_file="debian/metapackage/jellyfin"
-sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
+sed -i "s/${old_version_sed}/${new_version_pkg}/g" ${debian_equivs_file}
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
-echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium
+echo -e "jellyfin-server (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
@@ -97,7 +108,7 @@ pushd ${fedora_spec_temp_dir}
# Split out the stuff before and after changelog
csplit jellyfin.spec "/^%changelog/" # produces xx00 xx01
# Update the version in xx00
-sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
+sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
# Remove the header from xx01
sed -i '/^%changelog/d' xx01
# Create new temp file with our changelog
@@ -114,5 +125,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file}
rm -rf ${fedora_spec_temp_dir}
# Stage the changed files for commit
-git add ${shared_version_file} ${build_file} ${debian_equivs_file} ${debian_changelog_file} ${fedora_spec_file}
-git status
+git add .
+git status -v
diff --git a/debian/bin/restart.sh b/debian/bin/restart.sh
index 34fce0670..4847b918b 100755
--- a/debian/bin/restart.sh
+++ b/debian/bin/restart.sh
@@ -11,23 +11,43 @@
#
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
-get_service_command() {
- for command in systemctl service; do
- if which $command &>/dev/null; then
- echo $command && return
+# This is the Right Way(tm) to check if we are booted with
+# systemd, according to sd_booted(3)
+if [ -d /run/systemd/system ]; then
+ cmd=systemctl
+else
+ # Everything else is really hard to figure out, so we just use
+ # service(8) if it's available - that works with most init
+ # systems/distributions I know of, including FreeBSD
+ if type service >/dev/null 2>&1; then
+ cmd=service
+ else
+ # If even service(8) isn't available, we just try /etc/init.d
+ # and hope for the best
+ if [ -d /etc/init.d ]; then
+ cmd=sysv
+ else
+ echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
+ echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
+ exit 1
fi
- done
- echo "sysv"
-}
+ fi
+fi
+
+if type sudo >/dev/null 2>&1; then
+ sudo_command=sudo
+else
+ sudo_command=
+fi
-cmd="$( get_service_command )"
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
case $cmd in
'systemctl')
- echo "sleep 0.5; /usr/bin/sudo $( which systemctl ) start jellyfin" | at now
+ # Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
+ $sudo_command systemd-run systemctl restart jellyfin
;;
'service')
- echo "sleep 0.5; /usr/bin/sudo $( which service ) jellyfin start" | at now
+ echo "sleep 0.5; $sudo_command service jellyfin start" | at now
;;
'sysv')
echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
diff --git a/debian/conf/jellyfin b/debian/conf/jellyfin
index 9ebaf2bd8..ab8d5d1d4 100644
--- a/debian/conf/jellyfin
+++ b/debian/conf/jellyfin
@@ -33,6 +33,9 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# [OPTIONAL] run Jellyfin without the web app
#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
+# Space to add additional command line options to jellyfin (for help see ~$ jellyfin --help)
+JELLYFIN_ADDITIONAL_OPTS=""
+
# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
# 0 = Workstation
# 1 = Server
@@ -45,4 +48,4 @@ JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
# Application username
JELLYFIN_USER="jellyfin"
# Full application command
-JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT"
+JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
diff --git a/debian/conf/jellyfin-sudoers b/debian/conf/jellyfin-sudoers
index b481ba4ad..f84e7454f 100644
--- a/debian/conf/jellyfin-sudoers
+++ b/debian/conf/jellyfin-sudoers
@@ -2,9 +2,9 @@
Cmnd_Alias RESTARTSERVER_SYSV = /sbin/service jellyfin restart, /usr/sbin/service jellyfin restart
Cmnd_Alias STARTSERVER_SYSV = /sbin/service jellyfin start, /usr/sbin/service jellyfin start
Cmnd_Alias STOPSERVER_SYSV = /sbin/service jellyfin stop, /usr/sbin/service jellyfin stop
-Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemctl restart jellyfin, /bin/systemctl restart jellyfin
-Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemctl start jellyfin, /bin/systemctl start jellyfin
-Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemctl stop jellyfin, /bin/systemctl stop jellyfin
+Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
+Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
+Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
Cmnd_Alias RESTARTSERVER_INITD = /etc/init.d/jellyfin restart
Cmnd_Alias STARTSERVER_INITD = /etc/init.d/jellyfin start
Cmnd_Alias STOPSERVER_INITD = /etc/init.d/jellyfin stop
diff --git a/debian/control b/debian/control
index 9675d36ca..da9aa94d4 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
Build-Depends: debhelper (>= 9),
- dotnet-sdk-5.0,
+ dotnet-sdk-6.0,
libc6-dev,
libcurl4-openssl-dev,
libfontconfig1-dev,
@@ -23,6 +23,6 @@ Depends: at,
libfontconfig1,
libfreetype6,
libssl1.1
-Recommends: jellyfin-web
+Recommends: jellyfin-web, sudo
Description: Jellyfin is the Free Software Media System.
This package provides the Jellyfin server backend and API.
diff --git a/debian/jellyfin.service b/debian/jellyfin.service
index f1a8f4652..b86f40473 100644
--- a/debian/jellyfin.service
+++ b/debian/jellyfin.service
@@ -1,14 +1,48 @@
[Unit]
Description = Jellyfin Media Server
-After = network.target
+After = network-online.target
[Service]
Type = simple
EnvironmentFile = /etc/default/jellyfin
User = jellyfin
-ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
+ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS}
Restart = on-failure
TimeoutSec = 15
+NoNewPrivileges=true
+SystemCallArchitectures=native
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+RestrictNamespaces=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+ProtectControlGroups=true
+ProtectHostname=true
+ProtectKernelLogs=true
+ProtectKernelModules=true
+ProtectKernelTunables=true
+LockPersonality=true
+PrivateTmp=true
+PrivateDevices=false
+PrivateUsers=true
+RemoveIPC=true
+SystemCallFilter=~@clock
+SystemCallFilter=~@aio
+SystemCallFilter=~@chown
+SystemCallFilter=~@cpu-emulation
+SystemCallFilter=~@debug
+SystemCallFilter=~@keyring
+SystemCallFilter=~@memlock
+SystemCallFilter=~@module
+SystemCallFilter=~@mount
+SystemCallFilter=~@obsolete
+SystemCallFilter=~@privileged
+SystemCallFilter=~@raw-io
+SystemCallFilter=~@reboot
+SystemCallFilter=~@setuid
+SystemCallFilter=~@swap
+SystemCallErrorNumber=EPERM
+
+
[Install]
WantedBy = multi-user.target
diff --git a/debian/rules b/debian/rules
index 96541f41b..64e2b48ea 100755
--- a/debian/rules
+++ b/debian/rules
@@ -39,7 +39,7 @@ override_dh_auto_test:
override_dh_clistrip:
override_dh_auto_build:
- dotnet publish --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
+ dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
override_dh_auto_clean:
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 01fc1aaac..708c706b5 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -2,28 +2,28 @@ FROM centos:7
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare CentOS environment
-RUN yum update -y \
- && yum install -y epel-release \
- && yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git
+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
# Install DotNET SDK
-RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
- && rpmdev-setuptree \
- && yum install -y dotnet-sdk-${SDK_VERSION}
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \
- && mkdir -p ${SOURCE_DIR}/SPECS \
- && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
- && mkdir -p ${SOURCE_DIR}/SOURCES \
- && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
+ && 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 4c426b6d5..daba0eb7d 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,11 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts build-essential mmv \
+ libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev \
+ libssl1.1 liblttng-ust0
# 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 7ed6d52bc..db4e7f817 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,23 +10,24 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts build-essential mmv
# Prepare the cross-toolchain
RUN dpkg --add-architecture arm64 \
- && apt-get update \
- && apt-get install -y cross-gcc-dev \
- && TARGET_LIST="arm64" cross-gcc-gensource 8 \
- && cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \
- && apt-get install -y gcc-8-source libstdc++-8-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++-8-dev: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 \
+ 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
# 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 b46cceaa4..9b008e7fb 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,23 +10,25 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts build-essential mmv
# Prepare the cross-toolchain
RUN dpkg --add-architecture armhf \
- && apt-get update \
- && apt-get install -y cross-gcc-dev \
- && TARGET_LIST="armhf" cross-gcc-gensource 8 \
- && cd cross-gcc-packages-amd64/cross-gcc-8-armhf \
- && apt-get install -y gcc-8-source libstdc++-8-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++-8-dev: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 \
+ 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
# 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 0b1a57014..b2bd40713 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index 583f53ca0..fc60f1624 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index 177c11713..f5cc47d83 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -1,6 +1,4 @@
-ARG DOTNET_VERSION=5.0
-
-FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-buster-slim
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 137e56ecf..30615cd42 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -2,25 +2,28 @@ FROM fedora:33
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare Fedora environment
-RUN dnf update -y \
- && dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd
+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
# Install DotNET SDK
-RUN dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+ && mkdir -p dotnet-sdk \
+ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
+ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
+
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \
- && mkdir -p ${SOURCE_DIR}/SPECS \
- && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
- && mkdir -p ${SOURCE_DIR}/SOURCES \
- && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES
+ && 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 a0e23557a..2c7e41cac 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,11 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts unzip \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# 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 af0f55f8e..e903cf1d3 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,11 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts unzip \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# 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 ba004bb6a..0dd3c5e4e 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,11 @@ ENV ARCH=arm64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts unzip \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# 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 0d1114c01..16a8218e1 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,11 @@ ENV ARCH=armhf
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts unzip \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Link to docker-build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.armhf /build.sh
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index b57dc53f5..699ab2d40 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,11 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Link to docker-build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.macos /build.sh
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 3783dfacf..b567d7bce 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -10,15 +9,11 @@ ENV DEB_BUILD_OPTIONS=noddebs
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# 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 663a7af9e..ccfaaa5f0 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,15 +10,17 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https 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
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-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
# 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 83eb24e42..988c8f16d 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,34 +10,40 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg wget ca-certificates devscripts \
+ mmv build-essential lsb-release
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-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 \
- && apt-get install -y 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 -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev 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
+ && 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 \
+ 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
# 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 1187f37b9..61a008d6a 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -2,7 +2,6 @@ FROM ubuntu:bionic
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -11,34 +10,40 @@ ENV ARCH=amd64
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg wget ca-certificates devscripts \
+ mmv build-essential lsb-release
# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/ede8a287-3d61-4988-a356-32ff9129079e/bdb47b6b510ed0c4f0b132f7f4ad9d5a/dotnet-sdk-6.0.101-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 \
- && apt-get install -y 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 -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev 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
+ && 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 \
+ 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
# 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 8b2361f0b..b9543a7c9 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -1,8 +1,7 @@
-FROM debian:10
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=5.0
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
@@ -10,15 +9,11 @@ ENV DEB_BUILD_OPTIONS=noddebs
ENV IS_DOCKER=YES
# Prepare Debian build environment
-RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
-
-# Install dotnet repository
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/e1c236ec-c392-4eaa-a846-c600c82bb7f6/b13bd8b69f875f87cf83fc6f5457bcdf/dotnet-sdk-5.0.301-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
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts unzip \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
# 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 69f0cadcf..bfdc6e591 100755
--- a/deployment/build.centos.amd64
+++ b/deployment/build.centos.amd64
@@ -8,6 +8,16 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+ pushd fedora
+
+ cp -a jellyfin.spec /tmp/spec.orig
+ sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
+
+ popd
+fi
+
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
@@ -37,4 +47,13 @@ fi
rm -f fedora/jellyfin*.tar.gz
+if [[ ${IS_DOCKER} == YES ]]; then
+ pushd fedora
+
+ cp -a /tmp/spec.orig jellyfin.spec
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+
+ popd
+fi
+
popd
diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64
index 145e28d87..b2bbf9c29 100755
--- a/deployment/build.debian.amd64
+++ b/deployment/build.debian.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64
index 5699133a0..02f84471e 100755
--- a/deployment/build.debian.arm64
+++ b/deployment/build.debian.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf
index 20af2ddfb..92779cb59 100755
--- a/deployment/build.debian.armhf
+++ b/deployment/build.debian.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64
index 2c7bff506..23c5ed86a 100755
--- a/deployment/build.fedora.amd64
+++ b/deployment/build.fedora.amd64
@@ -8,6 +8,16 @@ set -o xtrace
# Move to source directory
pushd ${SOURCE_DIR}
+if [[ ${IS_DOCKER} == YES ]]; then
+ # Remove BuildRequires for dotnet-sdk-6.0, since it's installed manually
+ pushd fedora
+
+ cp -a jellyfin.spec /tmp/spec.orig
+ sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec
+
+ popd
+fi
+
# Modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
@@ -37,4 +47,13 @@ fi
rm -f fedora/jellyfin*.tar.gz
+if [[ ${IS_DOCKER} == YES ]]; then
+ pushd fedora
+
+ cp -a /tmp/spec.orig jellyfin.spec
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+
+ popd
+fi
+
popd
diff --git a/deployment/build.portable b/deployment/build.portable
index ea40ade5d..a6c741881 100755
--- a/deployment/build.portable
+++ b/deployment/build.portable
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=false"
tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64
index 0c29286c0..c36978c9e 100755
--- a/deployment/build.ubuntu.amd64
+++ b/deployment/build.ubuntu.amd64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64
index 65d67f80f..76d51e321 100755
--- a/deployment/build.ubuntu.arm64
+++ b/deployment/build.ubuntu.arm64
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf
index 370370abc..0ff5ab066 100755
--- a/deployment/build.ubuntu.armhf
+++ b/deployment/build.ubuntu.armhf
@@ -9,9 +9,9 @@ set -o xtrace
pushd ${SOURCE_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
- # Remove build-dep for dotnet-sdk-5.0, since it's installed manually
+ # Remove build-dep for dotnet-sdk-6.0, since it's installed manually
cp -a debian/control /tmp/control.orig
- sed -i '/dotnet-sdk-5.0,/d' debian/control
+ sed -i '/dotnet-sdk-6.0,/d' debian/control
fi
# Modify changelog to unstable configuration if IS_UNSTABLE
diff --git a/deployment/unraid/docker-templates/README.md b/deployment/unraid/docker-templates/README.md
index 2c268e8b3..8e401e009 100644
--- a/deployment/unraid/docker-templates/README.md
+++ b/deployment/unraid/docker-templates/README.md
@@ -8,7 +8,7 @@ Click on the Docker tab
Add the following line under "Template Repositories"
-https://github.com/jellyfin/jellyfin/blob/master/deployment/unraid/docker-templates
+https://github.com/jellyfin/jellyfin/tree/master/deployment/unraid/docker-templates
Click save than click on Add Container and select jellyfin.
diff --git a/fedora/Makefile b/fedora/Makefile
index 97904ddd3..22cc30448 100644
--- a/fedora/Makefile
+++ b/fedora/Makefile
@@ -1,26 +1,55 @@
-VERSION := $(shell sed -ne '/^Version:/s/.* *//p' fedora/jellyfin.spec)
+DIR := $(dir $(lastword $(MAKEFILE_LIST)))
+INSTGIT := $(shell if [ "$$(id -u)" = "0" ]; then dnf -y install git; fi)
+NAME := jellyfin-server
+VERSION := $(shell sed -ne '/^Version:/s/.* *//p' $(DIR)/jellyfin.spec)
+RELEASE := $(shell sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)/jellyfin.spec)
+GIT_VER := $(shell git describe --tags | sed -e 's/^v//' -e 's/-[0-9]*-g.*$$//')
+SRPM := jellyfin-$(subst -,~,$(GIT_VER))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
+TARBALL :=$(NAME)-$(subst -,~,$(GIT_VER)).tar.gz
-srpm:
- cd fedora/; \
- SOURCE_DIR=.. \
- WORKDIR="$${PWD}"; \
- tar \
- --transform "s,^\.,jellyfin-server-$(VERSION)," \
- --exclude='.git*' \
- --exclude='**/.git' \
- --exclude='**/.hg' \
- --exclude='**/.vs' \
- --exclude='**/.vscode' \
- --exclude='deployment' \
- --exclude='**/bin' \
- --exclude='**/obj' \
- --exclude='**/.nuget' \
- --exclude='*.deb' \
- --exclude='*.rpm' \
- --exclude='jellyfin-server-$(VERSION).tar.gz' \
- -czf "jellyfin-server-$(VERSION).tar.gz" \
+epel-7-x86_64_repos := https://packages.microsoft.com/rhel/7/prod/
+epel-8-x86_64_repos := https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/
+fedora_repos := https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/
+fedora-34-x86_64_repos := $(fedora_repos)
+fedora-35-x86_64_repos := $(fedora_repos)
+fedora-34-x86_64_repos := $(fedora_repos)
+
+outdir ?= $(PWD)/$(DIR)/
+TARGET ?= fedora-35-x86_64
+
+srpm: $(DIR)/$(SRPM)
+tarball: $(DIR)/$(TARBALL)
+
+$(DIR)/$(TARBALL):
+ cd $(DIR)/; \
+ SOURCE_DIR=.. \
+ WORKDIR="$${PWD}"; \
+ version=$(GIT_VER); \
+ tar \
+ --transform "s,^\.,$(NAME)-$(subst -,~,$(GIT_VER))," \
+ --exclude='.git*' \
+ --exclude='**/.git' \
+ --exclude='**/.hg' \
+ --exclude='**/.vs' \
+ --exclude='**/.vscode' \
+ --exclude=deployment \
+ --exclude='**/bin' \
+ --exclude='**/obj' \
+ --exclude='**/.nuget' \
+ --exclude='*.deb' \
+ --exclude='*.rpm' \
+ --exclude=$(notdir $@) \
+ -czf $(notdir $@) \
-C $${SOURCE_DIR} ./
- cd fedora/; \
- rpmbuild -bs jellyfin.spec \
- --define "_sourcedir $$PWD/" \
+
+$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin.spec
+ ./bump_version $(GIT_VER)
+ cd $(DIR)/; \
+ rpmbuild -bs jellyfin.spec \
+ --define "_sourcedir $$PWD/" \
--define "_srcrpmdir $(outdir)"
+
+rpms: $(DIR)/$(SRPM)
+ mock $(addprefix --addrepo=, $($(TARGET)_repos)) \
+ --enable-network \
+ -r $(TARGET) $<
diff --git a/fedora/jellyfin-server-lowports.conf b/fedora/jellyfin-server-lowports.conf
new file mode 100644
index 000000000..eeb48a4e4
--- /dev/null
+++ b/fedora/jellyfin-server-lowports.conf
@@ -0,0 +1,4 @@
+# This allows Jellyfin to bind to low ports such as 80 and/or 443
+
+[Service]
+AmbientCapabilities=CAP_NET_BIND_SERVICE \ No newline at end of file
diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service
index b092ebf2f..f706b0ad3 100644
--- a/fedora/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -1,5 +1,5 @@
[Unit]
-After=network.target
+After=network-online.target
Description=Jellyfin is a free software media system that puts you in control of managing and streaming your media.
[Service]
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 928fe590f..e93944a20 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -12,7 +12,7 @@ Release: 1%{?dist}
Summary: The Free Software Media System
License: GPLv3
URL: https://jellyfin.org
-# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
+# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
Source0: jellyfin-server-%{version}.tar.gz
Source11: jellyfin.service
Source12: jellyfin.env
@@ -20,6 +20,7 @@ Source13: jellyfin.sudoers
Source14: restart.sh
Source15: jellyfin.override.conf
Source16: jellyfin-firewalld.xml
+Source17: jellyfin-server-lowports.conf
%{?systemd_requires}
BuildRequires: systemd
@@ -27,7 +28,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
# Requirements not packaged in main repos
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/
-BuildRequires: dotnet-runtime-5.0, dotnet-sdk-5.0
+BuildRequires: dotnet-runtime-6.0, dotnet-sdk-6.0
Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
# Disable Automatic Dependency Processing
AutoReqProv: no
@@ -40,11 +41,21 @@ Jellyfin is a free software media system that puts you in control of managing an
Summary: The Free Software Media System Server backend
Requires(pre): shadow-utils
Requires: ffmpeg
-Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at
+Requires: libcurl, fontconfig, freetype, openssl, glibc, libicu, at, sudo
%description server
The Jellyfin media server backend.
+%package server-lowports
+# RPMfusion free
+Summary: The Free Software Media System Server backend. Low-port binding.
+Requires: jellyfin-server
+
+%description server-lowports
+The Jellyfin media server backend low port binding package. This package
+enables binding to ports < 1024. You would install this if you want
+the Jellyfin server to bind to ports 80 and/or 443 for example.
+
%prep
%autosetup -n jellyfin-server-%{version} -b 0
@@ -53,10 +64,12 @@ The Jellyfin media server backend.
%install
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+export PATH=$PATH:/usr/local/bin
dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
+%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
%{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
%{__mkdir} -p %{buildroot}%{_bindir}
tee %{buildroot}%{_bindir}/jellyfin << EOF
@@ -95,6 +108,9 @@ EOF
%attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin
%{_datadir}/licenses/jellyfin/LICENSE
+%files server-lowports
+%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
+
%pre server
getent group jellyfin >/dev/null || groupadd -r jellyfin
getent passwd jellyfin >/dev/null || \
@@ -137,6 +153,9 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
+* Mon Nov 29 2021 Brian J. Murrell <brian@interlinx.bc.ca>
+- Add jellyfin-server-lowports.service drop-in in a server-lowports
+ subpackage to allow binding to low ports
* Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
diff --git a/fedora/jellyfin.sudoers b/fedora/jellyfin.sudoers
index dd245af4b..57a9e7b67 100644
--- a/fedora/jellyfin.sudoers
+++ b/fedora/jellyfin.sudoers
@@ -1,8 +1,7 @@
# Allow jellyfin group to start, stop and restart itself
-Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemctl restart jellyfin, /bin/systemctl restart jellyfin
-Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemctl start jellyfin, /bin/systemctl start jellyfin
-Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemctl stop jellyfin, /bin/systemctl stop jellyfin
-
+Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
+Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
+Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
diff --git a/fedora/restart.sh b/fedora/restart.sh
index 34fce0670..4847b918b 100755
--- a/fedora/restart.sh
+++ b/fedora/restart.sh
@@ -11,23 +11,43 @@
#
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
-get_service_command() {
- for command in systemctl service; do
- if which $command &>/dev/null; then
- echo $command && return
+# This is the Right Way(tm) to check if we are booted with
+# systemd, according to sd_booted(3)
+if [ -d /run/systemd/system ]; then
+ cmd=systemctl
+else
+ # Everything else is really hard to figure out, so we just use
+ # service(8) if it's available - that works with most init
+ # systems/distributions I know of, including FreeBSD
+ if type service >/dev/null 2>&1; then
+ cmd=service
+ else
+ # If even service(8) isn't available, we just try /etc/init.d
+ # and hope for the best
+ if [ -d /etc/init.d ]; then
+ cmd=sysv
+ else
+ echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
+ echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
+ exit 1
fi
- done
- echo "sysv"
-}
+ fi
+fi
+
+if type sudo >/dev/null 2>&1; then
+ sudo_command=sudo
+else
+ sudo_command=
+fi
-cmd="$( get_service_command )"
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
case $cmd in
'systemctl')
- echo "sleep 0.5; /usr/bin/sudo $( which systemctl ) start jellyfin" | at now
+ # Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
+ $sudo_command systemd-run systemctl restart jellyfin
;;
'service')
- echo "sleep 0.5; /usr/bin/sudo $( which service ) jellyfin start" | at now
+ echo "sleep 0.5; $sudo_command service jellyfin start" | at now
;;
'sysv')
echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 791cb140d..6abdb7734 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -12,6 +12,13 @@
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="SharpFuzz" Version="1.6.2" />
</ItemGroup>
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
index a4a6f5f54..03b296494 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Program.cs
@@ -1,5 +1,12 @@
using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Library;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using Moq;
using SharpFuzz;
namespace Emby.Server.Implementations.Fuzz
@@ -11,6 +18,7 @@ namespace Emby.Server.Implementations.Fuzz
switch (args[0])
{
case "PathExtensions.TryReplaceSubPath": Run(PathExtensions_TryReplaceSubPath); return;
+ case "SqliteItemRepository.ItemImageInfoFromValueString": Run(SqliteItemRepository_ItemImageInfoFromValueString); return;
default: throw new ArgumentException($"Unknown fuzzing function: {args[0]}");
}
}
@@ -28,5 +36,27 @@ namespace Emby.Server.Implementations.Fuzz
_ = PathExtensions.TryReplaceSubPath(parts[0], parts[1], parts[2], out _);
}
+
+ private static void SqliteItemRepository_ItemImageInfoFromValueString(string data)
+ {
+ var sqliteItemRepository = MockSqliteItemRepository();
+ sqliteItemRepository.ItemImageInfoFromValueString(data);
+ }
+
+ private static SqliteItemRepository MockSqliteItemRepository()
+ {
+ const string VirtualMetaDataPath = "%MetadataPath%";
+ const string MetaDataPath = "/meta/data/path";
+
+ var appHost = new Mock<IServerApplicationHost>();
+ appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
+ .Returns((string x) => x.Replace(VirtualMetaDataPath, MetaDataPath, StringComparison.Ordinal));
+ appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
+ .Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
+
+ IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ fixture.Inject(appHost);
+ return fixture.Create<SqliteItemRepository>();
+ }
}
}
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt
new file mode 100644
index 000000000..1b0115882
--- /dev/null
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Testcases/SqliteItemRepository.ItemImageInfoFromValueString/test1.txt
@@ -0,0 +1 @@
+/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 44bc34369..7adc35087 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
- <!-- disable warning CA1040: Avoid empty interfaces -->
- <Rule Id="CA1040" Action="Info" />
-
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
@@ -12,6 +9,8 @@
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
+ <!-- disable warning SA1118: Parameter must not span multiple lines. -->
+ <Rule Id="SA1118" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
@@ -39,22 +38,51 @@
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
+ <!-- error on CA1063: Implement IDisposable correctly -->
+ <Rule Id="CA1063" Action="Error" />
+ <!-- error on CA1305: Specify IFormatProvider -->
+ <Rule Id="CA1305" Action="Error" />
+ <!-- error on CA1307: Specify StringComparison for clarity -->
+ <Rule Id="CA1307" Action="Error" />
+ <!-- error on CA1309: Use ordinal StringComparison -->
+ <Rule Id="CA1309" Action="Error" />
+ <!-- error on CA1725: Parameter names should match base declaration -->
+ <Rule Id="CA1725" Action="Error" />
+ <!-- error on CA1725: Call async methods when in an async method -->
+ <Rule Id="CA1727" Action="Error" />
+ <!-- error on CA1813: Avoid unsealed attributes -->
+ <Rule Id="CA1813" Action="Error" />
+ <!-- error on CA1843: Do not use 'WaitAll' with a single task -->
+ <Rule Id="CA1843" Action="Error" />
+ <!-- error on CA1845: Use span-based 'string.Concat' -->
+ <Rule Id="CA1845" Action="Error" />
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
+ <!-- error on CA2254: Template should be a static expression -->
+ <Rule Id="CA2254" Action="Error" />
+ <!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
+ <Rule Id="CA1014" Action="Info" />
<!-- disable warning CA1024: Use properties where appropriate -->
<Rule Id="CA1024" Action="Info" />
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
+ <!-- disable warning CA1040: Avoid empty interfaces -->
+ <Rule Id="CA1040" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
+ <!-- TODO: enable when false positives are fixed -->
+ <!-- disable warning CA1508: Avoid dead conditional code -->
+ <Rule Id="CA1508" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
+ <!-- disable warning CA1724: Type names should not match namespaces -->
+ <Rule Id="CA1724" Action="Info" />
<!-- disable warning CA1805: Do not initialize unnecessarily -->
<Rule Id="CA1805" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
@@ -65,11 +93,11 @@
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
+ <!-- disable warning CA2253: Named placeholders should not be numeric values -->
+ <Rule Id="CA2253" Action="Info" />
<!-- disable warning CA5394: Do not use insecure randomness -->
<Rule Id="CA5394" Action="Info" />
- <!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
- <Rule Id="CA1014" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
@@ -80,7 +108,11 @@
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
+ <!-- disable warning CA1848: Use the LoggerMessage delegates -->
+ <Rule Id="CA1848" Action="None" />
<!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments -->
<Rule Id="CA2101" Action="None" />
+ <!-- disable warning CA2234: Pass System.Uri objects instead of strings -->
+ <Rule Id="CA2234" Action="None" />
</Rules>
</RuleSet>
diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/src/Jellyfin.Extensions/AlphanumericComparator.cs
index e00cadca2..e3c81eba8 100644
--- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs
+++ b/src/Jellyfin.Extensions/AlphanumericComparator.cs
@@ -1,12 +1,19 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
-namespace MediaBrowser.Controller.Sorting
+namespace Jellyfin.Extensions
{
- public class AlphanumComparator : IComparer<string?>
+ /// <summary>
+ /// Alphanumeric <see cref="IComparer{T}" />.
+ /// </summary>
+ public class AlphanumericComparator : IComparer<string?>
{
+ /// <summary>
+ /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
+ /// </summary>
+ /// <param name="s1">The first object to compare.</param>
+ /// <param name="s2">The second object to compare.</param>
+ /// <returns>A signed integer that indicates the relative values of <c>x</c> and <c>y</c>.</returns>
public static int CompareValues(string? s1, string? s2)
{
if (s1 == null && s2 == null)
diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/src/Jellyfin.Extensions/CopyToExtensions.cs
index 2ecbc6539..72d37b5b6 100644
--- a/MediaBrowser.Common/Extensions/CopyToExtensions.cs
+++ b/src/Jellyfin.Extensions/CopyToExtensions.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
-namespace MediaBrowser.Common.Extensions
+namespace Jellyfin.Extensions
{
/// <summary>
/// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />.
diff --git a/src/Jellyfin.Extensions/DictionaryExtensions.cs b/src/Jellyfin.Extensions/DictionaryExtensions.cs
new file mode 100644
index 000000000..5bb828d01
--- /dev/null
+++ b/src/Jellyfin.Extensions/DictionaryExtensions.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+
+namespace Jellyfin.Extensions
+{
+ /// <summary>
+ /// Static extensions for the <see cref="IReadOnlyDictionary{TKey,TValue}"/> interface.
+ /// </summary>
+ public static class DictionaryExtensions
+ {
+ /// <summary>
+ /// Gets a string from a string dictionary, checking all keys sequentially,
+ /// stopping at the first key that returns a result that's neither null nor blank.
+ /// </summary>
+ /// <param name="dictionary">The dictionary.</param>
+ /// <param name="key1">The first checked key.</param>
+ /// <returns>System.String.</returns>
+ public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1)
+ {
+ return dictionary.GetFirstNotNullNorWhiteSpaceValue(key1, string.Empty, string.Empty);
+ }
+
+ /// <summary>
+ /// Gets a string from a string dictionary, checking all keys sequentially,
+ /// stopping at the first key that returns a result that's neither null nor blank.
+ /// </summary>
+ /// <param name="dictionary">The dictionary.</param>
+ /// <param name="key1">The first checked key.</param>
+ /// <param name="key2">The second checked key.</param>
+ /// <returns>System.String.</returns>
+ public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string key2)
+ {
+ return dictionary.GetFirstNotNullNorWhiteSpaceValue(key1, key2, string.Empty);
+ }
+
+ /// <summary>
+ /// Gets a string from a string dictionary, checking all keys sequentially,
+ /// stopping at the first key that returns a result that's neither null nor blank.
+ /// </summary>
+ /// <param name="dictionary">The dictionary.</param>
+ /// <param name="key1">The first checked key.</param>
+ /// <param name="key2">The second checked key.</param>
+ /// <param name="key3">The third checked key.</param>
+ /// <returns>System.String.</returns>
+ public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string key2, string key3)
+ {
+ if (dictionary.TryGetValue(key1, out var val) && !string.IsNullOrWhiteSpace(val))
+ {
+ return val;
+ }
+
+ if (!string.IsNullOrEmpty(key2) && dictionary.TryGetValue(key2, out val) && !string.IsNullOrWhiteSpace(val))
+ {
+ return val;
+ }
+
+ if (!string.IsNullOrEmpty(key3) && dictionary.TryGetValue(key3, out val) && !string.IsNullOrWhiteSpace(val))
+ {
+ return val;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs
index 2b8a6c395..b5fe93357 100644
--- a/MediaBrowser.Common/Extensions/EnumerableExtensions.cs
+++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs
@@ -1,7 +1,7 @@
-using System;
+using System;
using System.Collections.Generic;
-namespace MediaBrowser.Common.Extensions
+namespace Jellyfin.Extensions
{
/// <summary>
/// Static extensions for the <see cref="IEnumerable{T}"/> interface.
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
new file mode 100644
index 000000000..3d9538d1b
--- /dev/null
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PublishRepositoryUrl>true</PublishRepositoryUrl>
+ <EmbedUntrackedSources>true</EmbedUntrackedSources>
+ <IncludeSymbols>true</IncludeSymbols>
+ <SymbolPackageFormat>snupkg</SymbolPackageFormat>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Jellyfin Contributors</Authors>
+ <PackageId>Jellyfin.Extensions</PackageId>
+ <VersionPrefix>10.8.0</VersionPrefix>
+ <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+ <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+ <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
+ <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="../../SharedVersion.cs" />
+ </ItemGroup>
+
+ <!-- Code Analyzers-->
+ <ItemGroup>
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs
index b29e6a71a..c2543cf7c 100644
--- a/MediaBrowser.Common/Json/Converters/JsonBoolNumberConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonBoolNumberConverter.cs
@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a number to a boolean.
@@ -27,4 +27,4 @@ namespace MediaBrowser.Common.Json.Converters
writer.WriteBooleanValue(value);
}
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs
index 127a41a06..0d0cc2d06 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs
@@ -1,9 +1,4 @@
-using System;
-using System.ComponentModel;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert comma delimited string to array of type.
diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
index de41348dd..cc9311a24 100644
--- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json comma delimited array converter factory.
diff --git a/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs
index 73e3a0493..8ae080b15 100644
--- a/MediaBrowser.Common/Json/Converters/JsonDateTimeConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDateTimeConverter.cs
@@ -3,7 +3,7 @@ using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Legacy DateTime converter.
@@ -31,4 +31,4 @@ namespace MediaBrowser.Common.Json.Converters
}
}
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index b691798c9..321cfa502 100644
--- a/MediaBrowser.Common/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -3,13 +3,13 @@ using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
- public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]?>
+ public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]>
{
private readonly TypeConverter _typeConverter;
@@ -31,9 +31,9 @@ namespace MediaBrowser.Common.Json.Converters
{
if (reader.TokenType == JsonTokenType.String)
{
- // GetString can't return null here because we already handled it above
- var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
- if (stringEntries == null || stringEntries.Length == 0)
+ // null got handled higher up the call stack
+ var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
+ if (stringEntries.Length == 0)
{
return Array.Empty<T>();
}
@@ -44,7 +44,7 @@ namespace MediaBrowser.Common.Json.Converters
{
try
{
- parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
+ parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException();
convertedCount++;
}
catch (FormatException)
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
new file mode 100644
index 000000000..ea6d141cb
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters
+{
+ /// <summary>
+ /// Converts a GUID object or value to/from JSON.
+ /// </summary>
+ public class JsonGuidConverter : JsonConverter<Guid>
+ {
+ /// <inheritdoc />
+ public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => reader.TokenType == JsonTokenType.Null
+ ? Guid.Empty
+ : ReadInternal(ref reader);
+
+ // TODO: optimize by parsing the UTF8 bytes instead of converting to string first
+ internal static Guid ReadInternal(ref Utf8JsonReader reader)
+ => Guid.Parse(reader.GetString()!); // null got handled higher up the call stack
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
+ => WriteInternal(writer, value);
+
+ internal static void WriteInternal(Utf8JsonWriter writer, Guid value)
+ => writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
+ }
+}
diff --git a/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs
index 7c627f0e3..cd582ced6 100644
--- a/MediaBrowser.Model/Entities/JsonLowerCaseConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs
@@ -1,12 +1,8 @@
-#nullable disable
-// THIS IS A HACK
-// TODO: @bond Move to separate project
-
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Model.Entities
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts an object to a lowercase string.
@@ -15,7 +11,7 @@ namespace MediaBrowser.Model.Entities
public class JsonLowerCaseConverter<T> : JsonConverter<T>
{
/// <inheritdoc />
- public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
@@ -23,7 +19,7 @@ namespace MediaBrowser.Model.Entities
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
- writer.WriteStringValue(value?.ToString().ToLowerInvariant());
+ writer.WriteStringValue(value?.ToString()?.ToLowerInvariant());
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
index 6d96d5496..b477bcb66 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNullableGuidConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
@@ -1,9 +1,8 @@
using System;
-using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a GUID object or value to/from JSON.
@@ -12,21 +11,19 @@ namespace MediaBrowser.Common.Json.Converters
{
/// <inheritdoc />
public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var guidStr = reader.GetString();
- return guidStr == null ? null : new Guid(guidStr);
- }
+ => JsonGuidConverter.ReadInternal(ref reader);
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
{
- if (value == null || value == Guid.Empty)
+ if (value == Guid.Empty)
{
writer.WriteNullValue();
}
else
{
- writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture));
+ // null got handled higher up the call stack
+ JsonGuidConverter.WriteInternal(writer, value!.Value);
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
index 0501f7b2a..28437023f 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a nullable struct or value to/from JSON.
@@ -15,13 +15,10 @@ namespace MediaBrowser.Common.Json.Converters
/// <inheritdoc />
public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- if (reader.TokenType == JsonTokenType.Null)
- {
- return null;
- }
-
// Token is empty string.
- if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
+ if (reader.TokenType == JsonTokenType.String
+ && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty)
+ || (!reader.HasValueSequence && reader.ValueSpan.IsEmpty)))
{
return null;
}
@@ -31,15 +28,6 @@ namespace MediaBrowser.Common.Json.Converters
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
- {
- if (value.HasValue)
- {
- JsonSerializer.Serialize(writer, value.Value, options);
- }
- else
- {
- writer.WriteNullValue();
- }
- }
+ => JsonSerializer.Serialize(writer, value!.Value, options); // null got handled higher up the call stack
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs
index e2a3d798a..e7749589a 100644
--- a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverterFactory.cs
@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json nullable struct converter factory.
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs
index a8f6cfbec..6e59fe464 100644
--- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs
@@ -1,9 +1,4 @@
-using System;
-using System.ComponentModel;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert Pipe delimited string to array of type.
diff --git a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
index 1bebc49ec..579674f18 100644
--- a/MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json Pipe delimited array converter factory.
diff --git a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs
index 6cd980e48..36b36c9b4 100644
--- a/MediaBrowser.Common/Json/Converters/JsonStringConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs
@@ -4,7 +4,7 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converter to allow the serializer to read strings.
@@ -13,20 +13,11 @@ namespace MediaBrowser.Common.Json.Converters
{
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- return reader.TokenType switch
- {
- JsonTokenType.Null => null,
- JsonTokenType.String => reader.GetString(),
- _ => GetRawValue(reader)
- };
- }
+ => reader.TokenType == JsonTokenType.String ? reader.GetString() : GetRawValue(reader);
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
- {
- writer.WriteStringValue(value);
- }
+ => writer.WriteStringValue(value);
private static string GetRawValue(Utf8JsonReader reader)
{
diff --git a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs
index 81c093c54..51ffec1cb 100644
--- a/MediaBrowser.Common/Json/Converters/JsonVersionConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonVersionConverter.cs
@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace MediaBrowser.Common.Json.Converters
+namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a Version object or value to/from JSON.
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
index 405d6125f..f4ec91123 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
@@ -1,8 +1,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
-namespace MediaBrowser.Common.Json
+namespace Jellyfin.Extensions.Json
{
/// <summary>
/// Helper class for having compatible JSON throughout the codebase.
diff --git a/src/Jellyfin.Extensions/ReadOnlyListExtension.cs b/src/Jellyfin.Extensions/ReadOnlyListExtension.cs
new file mode 100644
index 000000000..7785cfb49
--- /dev/null
+++ b/src/Jellyfin.Extensions/ReadOnlyListExtension.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Extensions
+{
+ /// <summary>
+ /// Static extensions for the <see cref="IReadOnlyList{T}"/> interface.
+ /// </summary>
+ public static class ReadOnlyListExtension
+ {
+ /// <summary>
+ /// Finds the index of the desired item.
+ /// </summary>
+ /// <param name="source">The source list.</param>
+ /// <param name="value">The value to fine.</param>
+ /// <typeparam name="T">The type of item to find.</typeparam>
+ /// <returns>Index if found, else -1.</returns>
+ public static int IndexOf<T>(this IReadOnlyList<T> source, T value)
+ {
+ if (source is IList<T> list)
+ {
+ return list.IndexOf(value);
+ }
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ if (Equals(value, source[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /// <summary>
+ /// Finds the index of the predicate.
+ /// </summary>
+ /// <param name="source">The source list.</param>
+ /// <param name="match">The value to find.</param>
+ /// <typeparam name="T">The type of item to find.</typeparam>
+ /// <returns>Index if found, else -1.</returns>
+ public static int FindIndex<T>(this IReadOnlyList<T> source, Predicate<T> match)
+ {
+ if (source is List<T> list)
+ {
+ return list.FindIndex(match);
+ }
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ if (match(source[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs b/src/Jellyfin.Extensions/ShuffleExtensions.cs
index 2604abf85..33c492053 100644
--- a/MediaBrowser.Common/Extensions/ShuffleExtensions.cs
+++ b/src/Jellyfin.Extensions/ShuffleExtensions.cs
@@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
-namespace MediaBrowser.Common.Extensions
+namespace Jellyfin.Extensions
{
/// <summary>
/// Provides <c>Shuffle</c> extensions methods for <see cref="IList{T}" />.
/// </summary>
public static class ShuffleExtensions
{
- private static readonly Random _rng = new Random();
-
/// <summary>
/// Shuffles the items in a list.
/// </summary>
@@ -17,7 +15,7 @@ namespace MediaBrowser.Common.Extensions
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list)
{
- list.Shuffle(_rng);
+ list.Shuffle(Random.Shared);
}
/// <summary>
diff --git a/MediaBrowser.Common/Extensions/SplitStringExtensions.cs b/src/Jellyfin.Extensions/SplitStringExtensions.cs
index 9c9108495..5fa5c0123 100644
--- a/MediaBrowser.Common/Extensions/SplitStringExtensions.cs
+++ b/src/Jellyfin.Extensions/SplitStringExtensions.cs
@@ -1,4 +1,4 @@
-/*
+/*
MIT License
Copyright (c) 2019 Gérald Barré
@@ -22,13 +22,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-#pragma warning disable CS1591
-#pragma warning disable CA1034
+// TODO: remove when analyzer is fixed: https://github.com/dotnet/roslyn-analyzers/issues/5158
+#pragma warning disable CA1034 // Nested types should not be visible
+
using System;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
-namespace MediaBrowser.Common.Extensions
+namespace Jellyfin.Extensions
{
/// <summary>
/// Extension class for splitting lines without unnecessary allocations.
@@ -42,7 +43,7 @@ namespace MediaBrowser.Common.Extensions
/// <param name="separator">The separator to split on.</param>
/// <returns>The enumerator struct.</returns>
[Pure]
- public static SplitEnumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator);
+ public static Enumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator);
/// <summary>
/// Creates a new span split enumerator.
@@ -51,25 +52,44 @@ namespace MediaBrowser.Common.Extensions
/// <param name="separator">The separator to split on.</param>
/// <returns>The enumerator struct.</returns>
[Pure]
- public static SplitEnumerator Split(this ReadOnlySpan<char> str, char separator) => new (str, separator);
+ public static Enumerator Split(this ReadOnlySpan<char> str, char separator) => new (str, separator);
+ /// <summary>
+ /// Provides an enumerator for the substrings seperated by the separator.
+ /// </summary>
[StructLayout(LayoutKind.Auto)]
- public ref struct SplitEnumerator
+ public ref struct Enumerator
{
private readonly char _separator;
private ReadOnlySpan<char> _str;
- public SplitEnumerator(ReadOnlySpan<char> str, char separator)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Enumerator"/> struct.
+ /// </summary>
+ /// <param name="str">The span to split.</param>
+ /// <param name="separator">The separator to split on.</param>
+ public Enumerator(ReadOnlySpan<char> str, char separator)
{
_str = str;
_separator = separator;
Current = default;
}
+ /// <summary>
+ /// Gets a reference to the item at the current position of the enumerator.
+ /// </summary>
public ReadOnlySpan<char> Current { get; private set; }
- public readonly SplitEnumerator GetEnumerator() => this;
+ /// <summary>
+ /// Returns <c>this</c>.
+ /// </summary>
+ /// <returns><c>this</c>.</returns>
+ public readonly Enumerator GetEnumerator() => this;
+ /// <summary>
+ /// Advances the enumerator to the next item.
+ /// </summary>
+ /// <returns><c>true</c> if there is a next element; otherwise <c>false</c>.</returns>
public bool MoveNext()
{
if (_str.Length == 0)
diff --git a/MediaBrowser.Common/Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs
index 5cbf57d98..9751d9d42 100644
--- a/MediaBrowser.Common/Extensions/StreamExtensions.cs
+++ b/src/Jellyfin.Extensions/StreamExtensions.cs
@@ -3,7 +3,7 @@ using System.IO;
using System.Linq;
using System.Text;
-namespace MediaBrowser.Common.Extensions
+namespace Jellyfin.Extensions
{
/// <summary>
/// Class BaseExtensions.
diff --git a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs b/src/Jellyfin.Extensions/StringBuilderExtensions.cs
index 75d654f23..02ff7cc1f 100644
--- a/MediaBrowser.Common/Extensions/StringBuilderExtensions.cs
+++ b/src/Jellyfin.Extensions/StringBuilderExtensions.cs
@@ -1,7 +1,7 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Text;
-namespace MediaBrowser.Common.Extensions
+namespace Jellyfin.Extensions
{
/// <summary>
/// Extension methods for the <see cref="StringBuilder"/> class.
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
new file mode 100644
index 000000000..3a7707253
--- /dev/null
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -0,0 +1,65 @@
+using System;
+
+namespace Jellyfin.Extensions
+{
+ /// <summary>
+ /// Provides extensions methods for <see cref="string" />.
+ /// </summary>
+ public static class StringExtensions
+ {
+ /// <summary>
+ /// Counts the number of occurrences of [needle] in the string.
+ /// </summary>
+ /// <param name="value">The haystack to search in.</param>
+ /// <param name="needle">The character to search for.</param>
+ /// <returns>The number of occurrences of the [needle] character.</returns>
+ public static int Count(this ReadOnlySpan<char> value, char needle)
+ {
+ var count = 0;
+ var length = value.Length;
+ for (var i = 0; i < length; i++)
+ {
+ if (value[i] == needle)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ /// <summary>
+ /// Returns the part on the left of the <c>needle</c>.
+ /// </summary>
+ /// <param name="haystack">The string to seek.</param>
+ /// <param name="needle">The needle to find.</param>
+ /// <returns>The part left of the <paramref name="needle" />.</returns>
+ public static ReadOnlySpan<char> LeftPart(this ReadOnlySpan<char> haystack, char needle)
+ {
+ var pos = haystack.IndexOf(needle);
+ return pos == -1 ? haystack : haystack[..pos];
+ }
+
+ /// <summary>
+ /// Returns the part on the right of the <c>needle</c>.
+ /// </summary>
+ /// <param name="haystack">The string to seek.</param>
+ /// <param name="needle">The needle to find.</param>
+ /// <returns>The part right of the <paramref name="needle" />.</returns>
+ public static ReadOnlySpan<char> RightPart(this ReadOnlySpan<char> haystack, char needle)
+ {
+ var pos = haystack.LastIndexOf(needle);
+ if (pos == -1)
+ {
+ return haystack;
+ }
+
+ if (pos == haystack.Length - 1)
+ {
+ return ReadOnlySpan<char>.Empty;
+ }
+
+ return haystack[(pos + 1)..];
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index de03aa5f5..6f5c0ed0c 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -132,11 +132,13 @@ namespace Jellyfin.Api.Tests.Auth
authorizationInfo.User.AddDefaultPreferences();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
authorizationInfo.IsApiKey = false;
+ authorizationInfo.HasToken = true;
+ authorizationInfo.Token = "fake-token";
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>()))
- .Returns(authorizationInfo);
+ .Returns(Task.FromResult(authorizationInfo));
return authorizationInfo;
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
index a62fd8d5a..23c51999f 100644
--- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -4,6 +4,7 @@ using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
+using Jellyfin.Server.Implementations.Security;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -49,5 +50,61 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
+
+ [Theory]
+ [MemberData(nameof(GetParts_ValidAuthHeader_Success_Data))]
+ public void GetParts_ValidAuthHeader_Success(string input, Dictionary<string, string> parts)
+ {
+ var dict = AuthorizationContext.GetParts(input);
+ foreach (var (key, value) in parts)
+ {
+ Assert.Equal(dict[key], value);
+ }
+ }
+
+ private static TheoryData<string, Dictionary<string, string>> GetParts_ValidAuthHeader_Success_Data()
+ {
+ var data = new TheoryData<string, Dictionary<string, string>>();
+
+ data.Add(
+ "x=\"123,123\",y=\"123\"",
+ new Dictionary<string, string>
+ {
+ { "x", "123,123" },
+ { "y", "123" }
+ });
+
+ data.Add(
+ "x=\"123,123\", y=\"123\",z=\"'hi'\"",
+ new Dictionary<string, string>
+ {
+ { "x", "123,123" },
+ { "y", "123" },
+ { "z", "'hi'" }
+ });
+
+ data.Add(
+ "x=\"ab\"",
+ new Dictionary<string, string>
+ {
+ { "x", "ab" }
+ });
+
+ data.Add(
+ "param=Hörbücher",
+ new Dictionary<string, string>
+ {
+ { "param", "Hörbücher" }
+ });
+
+ data.Add(
+ "param=%22%Hörbücher",
+ new Dictionary<string, string>
+ {
+ { "param", "\"%Hörbücher" }
+ });
+
+ return data;
+ }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
index 117083815..1f06e8fde 100644
--- a/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Controllers/DynamicHlsControllerTests.cs
@@ -1,13 +1,5 @@
using System;
-using System.Collections.Generic;
-using AutoFixture;
-using AutoFixture.AutoMoq;
using Jellyfin.Api.Controllers;
-using Jellyfin.Api.Helpers;
-using Jellyfin.Api.Models.StreamingDtos;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
-using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers
@@ -26,33 +18,28 @@ namespace Jellyfin.Api.Tests.Controllers
}
}
- public static IEnumerable<object[]> GetSegmentLengths_Success_TestData()
+ public static TheoryData<long, int, double[]> GetSegmentLengths_Success_TestData()
{
- yield return new object[] { 0, 6, Array.Empty<double>() };
- yield return new object[]
- {
+ var data = new TheoryData<long, int, double[]>();
+ data.Add(0, 6, Array.Empty<double>());
+ data.Add(
TimeSpan.FromSeconds(3).Ticks,
6,
- new double[] { 3 }
- };
- yield return new object[]
- {
+ new double[] { 3 });
+ data.Add(
TimeSpan.FromSeconds(6).Ticks,
6,
- new double[] { 6 }
- };
- yield return new object[]
- {
+ new double[] { 6 });
+ data.Add(
TimeSpan.FromSeconds(3.3333333).Ticks,
6,
- new double[] { 3.3333333 }
- };
- yield return new object[]
- {
+ new double[] { 3.3333333 });
+ data.Add(
TimeSpan.FromSeconds(9.3333333).Ticks,
6,
- new double[] { 6, 3.3333333 }
- };
+ new double[] { 6, 3.3333333 });
+
+ return data;
}
}
}
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index 97e441b1d..c4640bd22 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -15,16 +15,16 @@ namespace Jellyfin.Api.Tests.Helpers
Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder));
}
- public static IEnumerable<object[]> GetOrderBy_Success_TestData()
+ public static TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]> GetOrderBy_Success_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<IReadOnlyList<string>, IReadOnlyList<SortOrder>, (string, SortOrder)[]>();
+
+ data.Add(
Array.Empty<string>(),
Array.Empty<SortOrder>(),
- Array.Empty<(string, SortOrder)>()
- };
- yield return new object[]
- {
+ Array.Empty<(string, SortOrder)>());
+
+ data.Add(
new string[]
{
"IsFavoriteOrLiked",
@@ -35,10 +35,9 @@ namespace Jellyfin.Api.Tests.Helpers
{
("IsFavoriteOrLiked", SortOrder.Ascending),
("Random", SortOrder.Ascending),
- }
- };
- yield return new object[]
- {
+ });
+
+ data.Add(
new string[]
{
"SortName",
@@ -52,38 +51,9 @@ namespace Jellyfin.Api.Tests.Helpers
{
("SortName", SortOrder.Descending),
("ProductionYear", SortOrder.Descending),
- }
- };
- }
-
- [Fact]
- public static void GetItemTypeStrings_Empty_Empty()
- {
- Assert.Empty(RequestHelpers.GetItemTypeStrings(Array.Empty<BaseItemKind>()));
- }
-
- [Fact]
- public static void GetItemTypeStrings_Valid_Success()
- {
- BaseItemKind[] input =
- {
- BaseItemKind.AggregateFolder,
- BaseItemKind.Audio,
- BaseItemKind.BasePluginFolder,
- BaseItemKind.CollectionFolder
- };
-
- string[] expected =
- {
- "AggregateFolder",
- "Audio",
- "BasePluginFolder",
- "CollectionFolder"
- };
-
- var res = RequestHelpers.GetItemTypeStrings(input);
+ });
- Assert.Equal(expected, res);
+ return data;
}
}
}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index d4ea91872..bcbe9c1be 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -6,11 +6,8 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -18,12 +15,12 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs b/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs
deleted file mode 100644
index 9903409fa..000000000
--- a/tests/Jellyfin.Common.Tests/Extensions/CopyToExtensionsTests.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Common.Extensions;
-using Xunit;
-
-namespace Jellyfin.Common.Tests.Extensions
-{
- public static class CopyToExtensionsTests
- {
- public static IEnumerable<object[]> CopyTo_Valid_Correct_TestData()
- {
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } };
- yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } };
- }
-
- [Theory]
- [MemberData(nameof(CopyTo_Valid_Correct_TestData))]
- public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected)
- {
- source.CopyTo(destination, index);
- Assert.Equal(expected, destination);
- }
-
- public static IEnumerable<object[]> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
- {
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 };
- yield return new object[] { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 };
- yield return new object[] { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 };
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 };
- yield return new object[] { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 };
- }
-
- [Theory]
- [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))]
- public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index)
- {
- Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index));
- }
- }
-}
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 546b2487e..ce607b2ec 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -6,20 +6,17 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
- <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs
new file mode 100644
index 000000000..463e17ad3
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs
@@ -0,0 +1,88 @@
+using System;
+using MediaBrowser.Controller.BaseItemManager;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Configuration;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Controller.Tests
+{
+ public class BaseItemManagerTests
+ {
+ [Theory]
+ [InlineData(typeof(Book), "LibraryEnabled", true)]
+ [InlineData(typeof(Book), "LibraryDisabled", false)]
+ [InlineData(typeof(MusicArtist), "Enabled", true)]
+ [InlineData(typeof(MusicArtist), "ServerDisabled", false)]
+ public void IsMetadataFetcherEnabled_ChecksOptions_ReturnsExpected(Type itemType, string fetcherName, bool expected)
+ {
+ BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!;
+
+ var libraryOptions = new LibraryOptions
+ {
+ TypeOptions = new[]
+ {
+ new TypeOptions
+ {
+ Type = "Book",
+ MetadataFetchers = new[] { "LibraryEnabled" }
+ }
+ }
+ };
+
+ var serverConfiguration = new ServerConfiguration();
+ foreach (var typeConfig in serverConfiguration.MetadataOptions)
+ {
+ typeConfig.DisabledMetadataFetchers = new[] { "ServerDisabled" };
+ }
+
+ var serverConfigurationManager = new Mock<IServerConfigurationManager>();
+ serverConfigurationManager.Setup(scm => scm.Configuration)
+ .Returns(serverConfiguration);
+
+ var baseItemManager = new BaseItemManager(serverConfigurationManager.Object);
+ var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryOptions, fetcherName);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(typeof(Book), "LibraryEnabled", true)]
+ [InlineData(typeof(Book), "LibraryDisabled", false)]
+ [InlineData(typeof(MusicArtist), "Enabled", true)]
+ [InlineData(typeof(MusicArtist), "ServerDisabled", false)]
+ public void IsImageFetcherEnabled_ChecksOptions_ReturnsExpected(Type itemType, string fetcherName, bool expected)
+ {
+ BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!;
+
+ var libraryOptions = new LibraryOptions
+ {
+ TypeOptions = new[]
+ {
+ new TypeOptions
+ {
+ Type = "Book",
+ ImageFetchers = new[] { "LibraryEnabled" }
+ }
+ }
+ };
+
+ var serverConfiguration = new ServerConfiguration();
+ foreach (var typeConfig in serverConfiguration.MetadataOptions)
+ {
+ typeConfig.DisabledImageFetchers = new[] { "ServerDisabled" };
+ }
+
+ var serverConfigurationManager = new Mock<IServerConfigurationManager>();
+ serverConfigurationManager.Setup(scm => scm.Configuration)
+ .Returns(serverConfiguration);
+
+ var baseItemManager = new BaseItemManager(serverConfigurationManager.Object);
+ var actual = baseItemManager.IsImageFetcherEnabled(item, libraryOptions, fetcherName);
+
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs b/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs
deleted file mode 100644
index 576c0a49b..000000000
--- a/tests/Jellyfin.Controller.Tests/Extensions/StringExtensionsTests.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using MediaBrowser.Controller.Extensions;
-using Xunit;
-
-namespace Jellyfin.Controller.Extensions.Tests
-{
- public class StringExtensionsTests
- {
- [Theory]
- [InlineData("", '_', 0)]
- [InlineData("___", '_', 3)]
- [InlineData("test\x00", '\x00', 1)]
- [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)]
- public void ReadOnlySpan_Count_Success(string str, char needle, int count)
- {
- Assert.Equal(count, str.AsSpan().Count(needle));
- }
- }
-}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 9a8ddafa0..0ffc19833 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -6,20 +6,17 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 1f6cd541c..098166001 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -1,20 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
index 0adf098c3..7730841a1 100644
--- a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
@@ -1,11 +1,10 @@
using System;
using System.Linq;
-using MediaBrowser.Controller.Sorting;
using Xunit;
-namespace Jellyfin.Controller.Tests
+namespace Jellyfin.Extensions.Tests
{
- public class AlphanumComparatorTests
+ public class AlphanumericComparatorTests
{
// InlineData is pre-sorted
[Theory]
@@ -20,10 +19,10 @@ namespace Jellyfin.Controller.Tests
[InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")]
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")]
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")]
- public void AlphanumComparatorTest(params string?[] strings)
+ public void AlphanumericComparatorTest(params string?[] strings)
{
var copy = strings.Reverse().ToArray();
- Array.Sort(copy, new AlphanumComparator());
+ Array.Sort(copy, new AlphanumericComparator());
Assert.True(strings.SequenceEqual(copy));
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
new file mode 100644
index 000000000..d46beedd9
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests
+{
+ public static class CopyToExtensionsTests
+ {
+ public static TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> CopyTo_Valid_Correct_TestData()
+ {
+ var data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>>();
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 });
+
+ data.Add(
+ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } );
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(CopyTo_Valid_Correct_TestData))]
+ public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected)
+ {
+ source.CopyTo(destination, index);
+ Assert.Equal(expected, destination);
+ }
+
+ public static TheoryData<IReadOnlyList<int>, IList<int>, int> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
+ {
+ var data = new TheoryData<IReadOnlyList<int>, IList<int>, int>();
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 );
+
+ data.Add(
+ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 );
+
+ data.Add(
+ new[] { 0, 1, 2 }, Array.Empty<int>(), 0 );
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 );
+
+ data.Add(
+ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 );
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))]
+ public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index)
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
new file mode 100644
index 000000000..ee3af7559
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="coverlet.collector" Version="3.1.0">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
+ </ItemGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
+ <ProjectReference Include="../../src/Jellyfin.Extensions/Jellyfin.Extensions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
index 7629d9912..125229ff9 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
@@ -1,11 +1,10 @@
-using System.Globalization;
using System.Text.Json;
using FsCheck;
using FsCheck.Xunit;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public class JsonBoolNumberTests
{
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
index ca300401d..f2ca2ff08 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedArrayTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
@@ -1,11 +1,11 @@
-using System;
+using System;
using System.Text.Json;
using System.Text.Json.Serialization;
-using Jellyfin.Common.Tests.Models;
+using Jellyfin.Extensions.Tests.Json.Models;
using MediaBrowser.Model.Session;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public static class JsonCommaDelimitedArrayTests
{
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
index 34ad9bac7..92886dcd2 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonCommaDelimitedIReadOnlyListTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
@@ -1,10 +1,10 @@
-using System.Text.Json;
+using System.Text.Json;
using System.Text.Json.Serialization;
-using Jellyfin.Common.Tests.Models;
+using Jellyfin.Extensions.Tests.Json.Models;
using MediaBrowser.Model.Session;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public static class JsonCommaDelimitedIReadOnlyListTests
{
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs
index dbfad3c2f..8465d465a 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonGuidConverterTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Text.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public class JsonGuidConverterTests
{
diff --git a/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
index 955d296cc..af9227de2 100644
--- a/tests/Jellyfin.Model.Tests/Entities/JsonLowerCaseConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonLowerCaseConverterTests.cs
@@ -1,9 +1,10 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Entities;
using Xunit;
-namespace Jellyfin.Model.Tests.Entities
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public class JsonLowerCaseConverterTests
{
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs
index cb3b66c4c..b0dbc09e4 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonNullableGuidConverterTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Text.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public class JsonNullableGuidConverterTests
{
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs
index 2b23c6705..655e07074 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonStringConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs
@@ -1,8 +1,8 @@
-using System.Text.Json;
-using MediaBrowser.Common.Json.Converters;
+using System.Text.Json;
+using Jellyfin.Extensions.Json.Converters;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public class JsonStringConverterTests
{
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs
index f2cefdbf8..5fbac7eab 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonVersionConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonVersionConverterTests.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using System.Text.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Extensions.Tests.Json.Converters
{
public class JsonVersionConverterTests
{
diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs
index 276e1bfbe..ef135278f 100644
--- a/tests/Jellyfin.Common.Tests/Models/GenericBodyArrayModel.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs
@@ -1,8 +1,8 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
-namespace Jellyfin.Common.Tests.Models
+namespace Jellyfin.Extensions.Tests.Json.Models
{
/// <summary>
/// The generic body model.
diff --git a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs
index 627454b25..8e7b5a35b 100644
--- a/tests/Jellyfin.Common.Tests/Models/GenericBodyIReadOnlyListModel.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs
@@ -1,8 +1,8 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json.Converters;
-namespace Jellyfin.Common.Tests.Models
+namespace Jellyfin.Extensions.Tests.Json.Models
{
/// <summary>
/// The generic body <c>IReadOnlyList</c> model.
diff --git a/tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs
index cbdbcf112..a73cfb078 100644
--- a/tests/Jellyfin.Common.Tests/Extensions/ShuffleExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/ShuffleExtensionsTests.cs
@@ -1,18 +1,15 @@
using System;
-using MediaBrowser.Common.Extensions;
using Xunit;
-namespace Jellyfin.Common.Tests.Extensions
+namespace Jellyfin.Extensions.Tests
{
public static class ShuffleExtensionsTests
{
- private static readonly Random _rng = new Random();
-
[Fact]
public static void Shuffle_Valid_Correct()
{
byte[] original = new byte[1 << 6];
- _rng.NextBytes(original);
+ Random.Shared.NextBytes(original);
byte[] shuffled = (byte[])original.Clone();
shuffled.Shuffle();
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
new file mode 100644
index 000000000..7186cc023
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -0,0 +1,41 @@
+using System;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests
+{
+ public class StringExtensionsTests
+ {
+ [Theory]
+ [InlineData("", '_', 0)]
+ [InlineData("___", '_', 3)]
+ [InlineData("test\x00", '\x00', 1)]
+ [InlineData("Imdb=tt0119567|Tmdb=330|TmdbCollection=328", '|', 2)]
+ public void ReadOnlySpan_Count_Success(string str, char needle, int count)
+ {
+ Assert.Equal(count, str.AsSpan().Count(needle));
+ }
+
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "Banana")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ [InlineData("Banana split 2", ' ', "Banana")]
+ public void LeftPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().LeftPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData("", 'q', "")]
+ [InlineData("Banana split", ' ', "split")]
+ [InlineData("Banana split", 'q', "Banana split")]
+ [InlineData("Banana split.", '.', "")]
+ [InlineData("Banana split 2", ' ', "2")]
+ public void RightPart_ValidArgsCharNeedle_Correct(string str, char needle, string expectedResult)
+ {
+ var result = str.AsSpan().RightPart(needle).ToString();
+ Assert.Equal(expectedResult, result);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
index 39fd8afda..c0c363d3d 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections;
-using System.Collections.Generic;
using MediaBrowser.MediaEncoding.Encoder;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@@ -9,15 +7,18 @@ namespace Jellyfin.MediaEncoding.Tests
{
public class EncoderValidatorTests
{
+ private readonly EncoderValidator _encoderValidator = new EncoderValidator(new NullLogger<EncoderValidatorTests>(), "ffmpeg");
+
[Theory]
[ClassData(typeof(GetFFmpegVersionTestData))]
public void GetFFmpegVersionTest(string versionOutput, Version? version)
{
- var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>());
- Assert.Equal(version, val.GetFFmpegVersion(versionOutput));
+ Assert.Equal(version, _encoderValidator.GetFFmpegVersionInternal(versionOutput));
}
[Theory]
+ [InlineData(EncoderValidatorTestsData.FFmpegV44Output, true)]
+ [InlineData(EncoderValidatorTestsData.FFmpegV432Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
[InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
@@ -28,25 +29,24 @@ namespace Jellyfin.MediaEncoding.Tests
[InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)]
public void ValidateVersionInternalTest(string versionOutput, bool valid)
{
- var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>());
- Assert.Equal(valid, val.ValidateVersionInternal(versionOutput));
+ Assert.Equal(valid, _encoderValidator.ValidateVersionInternal(versionOutput));
}
- private class GetFFmpegVersionTestData : IEnumerable<object?[]>
+ private class GetFFmpegVersionTestData : TheoryData<string, Version?>
{
- public IEnumerator<object?[]> GetEnumerator()
+ public GetFFmpegVersionTestData()
{
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0) };
- yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null };
+ Add(EncoderValidatorTestsData.FFmpegV44Output, new Version(4, 4));
+ Add(EncoderValidatorTestsData.FFmpegV432Output, new Version(4, 3, 2));
+ Add(EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1));
+ Add(EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3));
+ Add(EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1));
+ Add(EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2));
+ Add(EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4));
+ Add(EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4));
+ Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0));
+ Add(EncoderValidatorTestsData.FFmpegGitUnknownOutput, null);
}
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
index 9f5bef9a8..02bf046ed 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
@@ -2,6 +2,30 @@ namespace Jellyfin.MediaEncoding.Tests
{
internal static class EncoderValidatorTestsData
{
+ public const string FFmpegV44Output = @"ffmpeg version 4.4-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
+built with gcc 10.3.0 (Rev5, Built by MSYS2 project)
+configuration: --disable-static --enable-shared --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls
+libavutil 56. 70.100 / 56. 70.100
+libavcodec 58.134.100 / 58.134.100
+libavformat 58. 76.100 / 58. 76.100
+libavdevice 58. 13.100 / 58. 13.100
+libavfilter 7.110.100 / 7.110.100
+libswscale 5. 9.100 / 5. 9.100
+libswresample 3. 9.100 / 3. 9.100
+libpostproc 55. 9.100 / 55. 9.100";
+
+ public const string FFmpegV432Output = @"ffmpeg version n4.3.2-Jellyfin Copyright (c) 2000-2021 the FFmpeg developers
+built with gcc 10.2.0 (Rev9, Built by MSYS2 project)
+configuration: --disable-static --enable-shared --cc='ccache gcc' --cxx='ccache g++' --extra-version=Jellyfin --disable-ffplay --disable-debug --enable-lto --enable-gpl --enable-version3 --enable-bzlib --enable-iconv --enable-lzma --enable-zlib --enable-sdl2 --enable-fontconfig --enable-gmp --enable-libass --enable-libzimg --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libdav1d --enable-opencl --enable-dxva2 --enable-d3d11va --enable-amf --enable-libmfx --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvenc --enable-nvdec --enable-ffnvcodec --enable-gnutls
+libavutil 56. 51.100 / 56. 51.100
+libavcodec 58. 91.100 / 58. 91.100
+libavformat 58. 45.100 / 58. 45.100
+libavdevice 58. 10.100 / 58. 10.100
+libavfilter 7. 85.100 / 7. 85.100
+libswscale 5. 7.100 / 5. 7.100
+libswresample 3. 7.100 / 3. 7.100
+libpostproc 55. 7.100 / 55. 7.100";
+
public const string FFmpegV431Output = @"ffmpeg version n4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
built with gcc 10.1.0 (GCC)
configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-avisynth --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmfx --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-librav1e --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3
diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
index 415682e85..97dbb3be0 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs
@@ -1,8 +1,9 @@
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
+using MediaBrowser.Model.IO;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests
@@ -14,9 +15,10 @@ namespace Jellyfin.MediaEncoding.Tests
public async Task Test(string fileName)
{
var path = Path.Join("Test Data", fileName);
- using (var stream = File.OpenRead(path))
+ await using (var stream = AsyncFile.OpenRead(path))
{
- await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
+ var res = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(stream, JsonDefaults.Options).ConfigureAwait(false);
+ Assert.NotNull(res);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 6b828e113..dc4a42c19 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -6,11 +6,8 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -21,10 +18,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="AutoFixture" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+ <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 98fbb00d5..0fc8724b6 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -2,11 +2,13 @@ using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using Xunit;
namespace Jellyfin.MediaEncoding.Tests.Probing
@@ -16,6 +18,19 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly ProbeResultNormalizer _probeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), null);
+ [Theory]
+ [InlineData("2997/125", 23.976f)]
+ [InlineData("1/50", 0.02f)]
+ [InlineData("25/1", 25f)]
+ [InlineData("120/1", 120f)]
+ [InlineData("1704753000/71073479", 23.98578237601117f)]
+ [InlineData("0/0", null)]
+ [InlineData("1/1000", 0.001f)]
+ [InlineData("1/90000", 1.1111111E-05f)]
+ [InlineData("1/48000", 2.0833333E-05f)]
+ public void GetFrameRate_Success(string value, float? expected)
+ => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
+
[Fact]
public void GetMediaInfo_MetaData_Success()
{
@@ -56,6 +71,72 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
}
[Fact]
+ public void GetMediaInfo_Mp4MetaData_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_mp4_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+
+ // subtitle handling requires a localization object, set a mock to return the input string
+ var mockLocalization = new Mock<ILocalizationManager>();
+ mockLocalization.Setup(x => x.GetLocalizedString(It.IsAny<string>())).Returns<string>(x => x);
+ ProbeResultNormalizer localizedProbeResultNormalizer = new ProbeResultNormalizer(new NullLogger<EncoderValidatorTests>(), mockLocalization.Object);
+
+ MediaInfo res = localizedProbeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_mp4_metadata.mkv", MediaProtocol.File);
+
+ // [Video, Audio (Main), Audio (Commentary), Subtitle (Main, Spanish), Subtitle (Main, English), Subtitle (Commentary)
+ Assert.Equal(6, res.MediaStreams.Count);
+
+ Assert.NotNull(res.VideoStream);
+ Assert.Equal(res.MediaStreams[0], res.VideoStream);
+ Assert.Equal(0, res.VideoStream.Index);
+ Assert.Equal("h264", res.VideoStream.Codec);
+ Assert.Equal("High", res.VideoStream.Profile);
+ Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
+ Assert.Equal(358, res.VideoStream.Height);
+ Assert.Equal(720, res.VideoStream.Width);
+ Assert.Equal("2.40:1", res.VideoStream.AspectRatio);
+ Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
+ Assert.Equal(31d, res.VideoStream.Level);
+ Assert.Equal(1, res.VideoStream.RefFrames);
+ Assert.True(res.VideoStream.IsAVC);
+ Assert.Equal(120f, res.VideoStream.RealFrameRate);
+ Assert.Equal("1/90000", res.VideoStream.TimeBase);
+ Assert.Equal(1147365, res.VideoStream.BitRate);
+ Assert.Equal(8, res.VideoStream.BitDepth);
+ Assert.True(res.VideoStream.IsDefault);
+ Assert.Equal("und", res.VideoStream.Language);
+
+ Assert.Equal(MediaStreamType.Audio, res.MediaStreams[1].Type);
+ Assert.Equal("aac", res.MediaStreams[1].Codec);
+ Assert.Equal(7, res.MediaStreams[1].Channels);
+ Assert.True(res.MediaStreams[1].IsDefault);
+ Assert.Equal("eng", res.MediaStreams[1].Language);
+ Assert.Equal("Surround 6.1", res.MediaStreams[1].Title);
+
+ Assert.Equal(MediaStreamType.Audio, res.MediaStreams[2].Type);
+ Assert.Equal("aac", res.MediaStreams[2].Codec);
+ Assert.Equal(2, res.MediaStreams[2].Channels);
+ Assert.False(res.MediaStreams[2].IsDefault);
+ Assert.Equal("eng", res.MediaStreams[2].Language);
+ Assert.Equal("Commentary", res.MediaStreams[2].Title);
+
+ Assert.Equal("spa", res.MediaStreams[3].Language);
+ Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[3].Type);
+ Assert.Equal("DVDSUB", res.MediaStreams[3].Codec);
+ Assert.Null(res.MediaStreams[3].Title);
+
+ Assert.Equal("eng", res.MediaStreams[4].Language);
+ Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[4].Type);
+ Assert.Equal("mov_text", res.MediaStreams[4].Codec);
+ Assert.Null(res.MediaStreams[4].Title);
+
+ Assert.Equal("eng", res.MediaStreams[5].Language);
+ Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[5].Type);
+ Assert.Equal("mov_text", res.MediaStreams[5].Codec);
+ Assert.Equal("Commentary", res.MediaStreams[5].Title);
+ }
+
+ [Fact]
public void GetMediaInfo_MusicVideo_Success()
{
var bytes = File.ReadAllBytes("Test Data/Probing/music_video_metadata.json");
@@ -69,7 +150,60 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("Album", res.Album);
Assert.Equal(2021, res.ProductionYear);
Assert.True(res.PremiereDate.HasValue);
- Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo).ToUniversalTime(), res.PremiereDate);
+ Assert.Equal(DateTime.Parse("2021-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
+ }
+
+ [Fact]
+ public void GetMediaInfo_GivenOriginalDateContainsOnlyYear_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/music_year_only_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, null, true, "Test Data/Probing/music.flac", MediaProtocol.File);
+
+ Assert.Equal("Baker Street", res.Name);
+ Assert.Single(res.Artists);
+ Assert.Equal("Gerry Rafferty", res.Artists[0]);
+ Assert.Equal("City to City", res.Album);
+ Assert.Equal(1978, res.ProductionYear);
+ Assert.True(res.PremiereDate.HasValue);
+ Assert.Equal(DateTime.Parse("1978-01-01T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
+ Assert.Contains("Electronic", res.Genres);
+ Assert.Contains("Ambient", res.Genres);
+ Assert.Contains("Pop", res.Genres);
+ Assert.Contains("Jazz", res.Genres);
+ }
+
+ [Fact]
+ public void GetMediaInfo_Music_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/music_metadata.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, null, true, "Test Data/Probing/music.flac", MediaProtocol.File);
+
+ Assert.Equal("UP NO MORE", res.Name);
+ Assert.Single(res.Artists);
+ Assert.Equal("TWICE", res.Artists[0]);
+ Assert.Equal("Eyes wide open", res.Album);
+ Assert.Equal(2020, res.ProductionYear);
+ Assert.True(res.PremiereDate.HasValue);
+ Assert.Equal(DateTime.Parse("2020-10-26T00:00Z", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal), res.PremiereDate);
+ Assert.Equal(22, res.People.Length);
+ Assert.Equal("Krysta Youngs", res.People[0].Name);
+ Assert.Equal(PersonType.Composer, res.People[0].Type);
+ Assert.Equal("Julia Ross", res.People[1].Name);
+ Assert.Equal(PersonType.Composer, res.People[1].Type);
+ Assert.Equal("Yiwoomin", res.People[2].Name);
+ Assert.Equal(PersonType.Composer, res.People[2].Type);
+ Assert.Equal("Ji-hyo Park", res.People[3].Name);
+ Assert.Equal(PersonType.Lyricist, res.People[3].Type);
+ Assert.Equal("Yiwoomin", res.People[4].Name);
+ Assert.Equal(PersonType.Actor, res.People[4].Type);
+ Assert.Equal("Electric Piano", res.People[4].Role);
+ Assert.Equal(4, res.Genres.Length);
+ Assert.Contains("Electronic", res.Genres);
+ Assert.Contains("Trance", res.Genres);
+ Assert.Contains("Dance", res.Genres);
+ Assert.Contains("Jazz", res.Genres);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index 537a944b0..c07c9ea7d 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -31,5 +31,27 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
}
}
+
+ [Fact]
+ public void Parse_EmptyNewlineBetweenText_Success()
+ {
+ using (var stream = File.OpenRead("Test Data/example2.srt"))
+ {
+ var parsed = new SrtParser(new NullLogger<SrtParser>()).Parse(stream, CancellationToken.None);
+ Assert.Equal(2, parsed.TrackEvents.Count);
+
+ var trackEvent1 = parsed.TrackEvents[0];
+ Assert.Equal("311", trackEvent1.Id);
+ Assert.Equal(TimeSpan.Parse("00:16:46.465", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:16:49.009", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
+ Assert.Equal("Una vez que la gente se entere" + Environment.NewLine + Environment.NewLine + "de que ustedes están aquí,", trackEvent1.Text);
+
+ var trackEvent2 = parsed.TrackEvents[1];
+ Assert.Equal("312", trackEvent2.Id);
+ Assert.Equal(TimeSpan.Parse("00:16:49.092", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:16:51.470", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
+ Assert.Equal("este lugar se convertirá" + Environment.NewLine + Environment.NewLine + "en un maldito zoológico.", trackEvent2.Text);
+ }
+ }
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
index 5db80c300..56649db8f 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -38,10 +38,11 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
}
}
- public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
+ public static TheoryData<string, IReadOnlyList<SubtitleTrackEvent>> Parse_MultipleDialogues_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, IReadOnlyList<SubtitleTrackEvent>>();
+
+ data.Add(
@"[Events]
Format: Layer, Start, End, Text
Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
@@ -65,8 +66,9 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
StartPositionTicks = 31800000,
EndPositionTicks = 38500000
}
- }
- };
+ });
+
+ return data;
}
[Fact]
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
new file mode 100644
index 000000000..639c364df
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -0,0 +1,83 @@
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class SubtitleEncoderTests
+ {
+ internal static TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo> GetReadableFile_Valid_TestData()
+ {
+ var data = new TheoryData<MediaSourceInfo, MediaStream, SubtitleEncoder.SubtitleInfo>();
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ass",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ssa",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ssa", MediaProtocol.File, "ssa", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.File
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.srt",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.srt", MediaProtocol.File, "srt", true));
+
+ data.Add(
+ new MediaSourceInfo()
+ {
+ Protocol = MediaProtocol.Http
+ },
+ new MediaStream()
+ {
+ Path = "/media/sub.ass",
+ IsExternal = true
+ },
+ new SubtitleEncoder.SubtitleInfo("/media/sub.ass", MediaProtocol.File, "ass", true));
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetReadableFile_Valid_TestData))]
+ internal async Task GetReadableFile_Valid_Success(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleEncoder.SubtitleInfo subtitleInfo)
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ var subtitleEncoder = fixture.Create<SubtitleEncoder>();
+ var result = await subtitleEncoder.GetReadableFile(mediaSource, subtitleStream, CancellationToken.None).ConfigureAwait(false);
+ Assert.Equal(subtitleInfo.Path, result.Path);
+ Assert.Equal(subtitleInfo.Protocol, result.Protocol);
+ Assert.Equal(subtitleInfo.Format, result.Format);
+ Assert.Equal(subtitleInfo.IsExternal, result.IsExternal);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json
new file mode 100644
index 000000000..6530629fe
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_metadata.json
@@ -0,0 +1,144 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "flac",
+ "codec_long_name": "FLAC (Free Lossless Audio Codec)",
+ "codec_type": "audio",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "s16",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/44100",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 9447984,
+ "duration": "214.240000",
+ "bits_per_raw_sample": "16",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "mjpeg",
+ "codec_long_name": "Motion JPEG",
+ "profile": "Baseline",
+ "codec_type": "video",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 500,
+ "height": 500,
+ "coded_width": 500,
+ "coded_height": 500,
+ "closed_captions": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "1:1",
+ "pix_fmt": "yuvj420p",
+ "level": -99,
+ "color_range": "pc",
+ "color_space": "bt470bg",
+ "chroma_location": "center",
+ "refs": 1,
+ "r_frame_rate": "90000/1",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 19281600,
+ "duration": "214.240000",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 1,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "comment": "Cover (front)"
+ }
+ }
+ ],
+ "format": {
+ "filename": "03 UP NO MORE.flac",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "format_name": "flac",
+ "format_long_name": "raw FLAC",
+ "start_time": "0.000000",
+ "duration": "214.240000",
+ "size": "28714641",
+ "bit_rate": "1072242",
+ "probe_score": 100,
+ "tags": {
+ "MUSICBRAINZ_RELEASEGROUPID": "aa05ff10-8589-4c9c-a0d4-6b024f4e4556",
+ "ORIGINALDATE": "2020-10-26",
+ "ORIGINALYEAR": "2020",
+ "RELEASETYPE": "album",
+ "MUSICBRAINZ_ALBUMID": "222e6610-75c9-400e-8dc3-bb61f9fc5ca7",
+ "SCRIPT": "Latn",
+ "ALBUM": "Eyes wide open",
+ "RELEASECOUNTRY": "JP",
+ "BARCODE": "190295105280",
+ "LABEL": "JYP Entertainment",
+ "RELEASESTATUS": "official",
+ "DATE": "2020-10-26",
+ "MUSICBRAINZ_ALBUMARTISTID": "8da127cc-c432-418f-b356-ef36210d82ac",
+ "album_artist": "TWICE",
+ "ALBUMARTISTSORT": "TWICE",
+ "TOTALDISCS": "1",
+ "TOTALTRACKS": "13",
+ "MEDIA": "Digital Media",
+ "disc": "1",
+ "MUSICBRAINZ_TRACKID": "7d1a1044-b564-480d-9df3-22f9656fdb97",
+ "TITLE": "UP NO MORE",
+ "ISRC": "US5TA2000136",
+ "PERFORMER": "Yiwoomin (electric piano);Yiwoomin (synthesizer);Yiwoomin (bass);Yiwoomin (guitar);TWICE;Tzu-yu Chou (vocals);Momo Hirai (vocals);Na-yeon Im (vocals);Da-hyun Kim (vocals);Sana Minatozaki (vocals);Mina Myoui (vocals);Ji-hyo Park (vocals);Chae-young Son (vocals);Jeong-yeon Yoo (vocals);Perrie (background vocals)",
+ "MIXER": "Bong Won Shin",
+ "ARRANGER": "Krysta Youngs;Julia Ross;Yiwoomin",
+ "MUSICBRAINZ_WORKID": "02b37083-0337-4721-9f17-bf31971043e8",
+ "LANGUAGE": "kor;eng",
+ "WORK": "Up No More",
+ "COMPOSER": "Krysta Youngs;Julia Ross;Yiwoomin",
+ "COMPOSERSORT": "Krysta Youngs;Ross, Julia;Yiwoomin",
+ "LYRICIST": "Ji-hyo Park",
+ "MUSICBRAINZ_ARTISTID": "8da127cc-c432-418f-b356-ef36210d82ac",
+ "ARTIST": "TWICE",
+ "ARTISTSORT": "TWICE",
+ "ARTISTS": "TWICE",
+ "MUSICBRAINZ_RELEASETRACKID": "ad49b840-da9e-4e7c-924b-29fdee187052",
+ "track": "3",
+ "GENRE": "Electronic;Trance;Dance;Jazz",
+ "WEBSITE": "http://twice.jype.com/;http://www.twicejapan.com/",
+ "ACOUSTID_ID": "aae2e972-108c-4d0c-8e31-9d078283e3dc",
+ "MOOD": "Not acoustic;Not aggressive;Electronic;Happy;Party;Not relaxed;Not sad",
+ "TRACKTOTAL": "13",
+ "DISCTOTAL": "1"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json
new file mode 100644
index 000000000..ddf890c45
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/music_year_only_metadata.json
@@ -0,0 +1,147 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "flac",
+ "codec_long_name": "FLAC (Free Lossless Audio Codec)",
+ "codec_type": "audio",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "s16",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/44100",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 16394616,
+ "duration": "371.760000",
+ "bits_per_raw_sample": "16",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "mjpeg",
+ "codec_long_name": "Motion JPEG",
+ "profile": "Baseline",
+ "codec_type": "video",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 500,
+ "height": 498,
+ "coded_width": 500,
+ "coded_height": 498,
+ "closed_captions": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "250:249",
+ "pix_fmt": "yuvj420p",
+ "level": -99,
+ "color_range": "pc",
+ "color_space": "bt470bg",
+ "chroma_location": "center",
+ "refs": 1,
+ "r_frame_rate": "90000/1",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 33458400,
+ "duration": "371.760000",
+ "bits_per_raw_sample": "8",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 1,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "comment": "Cover (front)"
+ }
+ }
+ ],
+ "format": {
+ "filename": "02 Baker Street.flac",
+ "nb_streams": 2,
+ "nb_programs": 0,
+ "format_name": "flac",
+ "format_long_name": "raw FLAC",
+ "start_time": "0.000000",
+ "duration": "371.760000",
+ "size": "37072649",
+ "bit_rate": "797775",
+ "probe_score": 100,
+ "tags": {
+ "MUSICBRAINZ_RELEASEGROUPID": "238c3fb4-5792-342b-b217-02f66298b424",
+ "ORIGINALDATE": "1978",
+ "ORIGINALYEAR": "1978",
+ "RELEASETYPE": "album",
+ "MUSICBRAINZ_ALBUMID": "30156786-e511-3106-ac95-66f0e880b24b",
+ "ASIN": "B000007O5H",
+ "MUSICBRAINZ_ALBUMARTISTID": "563201cb-721c-4cfb-acca-c1ba69e3d1fb",
+ "album_artist": "Gerry Rafferty",
+ "ALBUMARTISTSORT": "Rafferty, Gerry",
+ "LABEL": "Liberty EMI Records UK",
+ "CATALOGNUMBER": "CDP 7 46049 2",
+ "DATE": "1989-07-26",
+ "RELEASECOUNTRY": "GB",
+ "BARCODE": "077774604925",
+ "ALBUM": "City to City",
+ "SCRIPT": "Latn",
+ "RELEASESTATUS": "official",
+ "TOTALDISCS": "1",
+ "disc": "1",
+ "MEDIA": "CD",
+ "TOTALTRACKS": "10",
+ "MUSICBRAINZ_TRACKID": "9235e22e-afbd-48f7-b329-21dae6da2810",
+ "ISRC": "GBAYE1100924;GBAYE7800619",
+ "PERFORMER": "Hugh Burns (electric guitar);Nigel Jenkins (electric guitar);Tommy Eyre (keyboard and Moog);Glen LeFleur (percussion);Raphael Ravenscroft (saxophone);Henry Spinetti (drums (drum set));Gary Taylor (bass);Gerry Rafferty (lead vocals)",
+ "ARRANGER": "Graham Preskett",
+ "MIXER": "Declan O’Doherty",
+ "PRODUCER": "Hugh Murphy;Gerry Rafferty",
+ "MUSICBRAINZ_WORKID": "a9eb3c45-784c-3c32-860c-4b406f03961b",
+ "LANGUAGE": "eng",
+ "WORK": "Baker Street",
+ "COMPOSER": "Gerry Rafferty",
+ "COMPOSERSORT": "Rafferty, Gerry",
+ "LYRICIST": "Gerry Rafferty",
+ "TITLE": "Baker Street",
+ "MUSICBRAINZ_ARTISTID": "563201cb-721c-4cfb-acca-c1ba69e3d1fb",
+ "ARTIST": "Gerry Rafferty",
+ "ARTISTSORT": "Rafferty, Gerry",
+ "ARTISTS": "Gerry Rafferty",
+ "MUSICBRAINZ_RELEASETRACKID": "407cf7f7-440d-3e76-8b89-8686198868ea",
+ "track": "2",
+ "GENRE": "Electronic;Ambient;Pop;Jazz",
+ "WEBSITE": "http://www.gerryrafferty.com/",
+ "ACOUSTID_ID": "68f8d979-a659-4aa0-a216-eb3721a951eb",
+ "MOOD": "Acoustic;Not aggressive;Not electronic;Not happy;Party;Relaxed;Not sad",
+ "TRACKTOTAL": "10",
+ "DISCTOTAL": "1"
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json
new file mode 100644
index 000000000..77e3def76
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_mp4_metadata.json
@@ -0,0 +1,260 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "profile": "High",
+ "codec_type": "video",
+ "codec_tag_string": "avc1",
+ "codec_tag": "0x31637661",
+ "width": 720,
+ "height": 358,
+ "coded_width": 720,
+ "coded_height": 358,
+ "closed_captions": 0,
+ "has_b_frames": 2,
+ "sample_aspect_ratio": "32:27",
+ "display_aspect_ratio": "1280:537",
+ "pix_fmt": "yuv420p",
+ "level": 31,
+ "color_range": "tv",
+ "color_space": "smpte170m",
+ "color_transfer": "bt709",
+ "color_primaries": "smpte170m",
+ "chroma_location": "left",
+ "refs": 1,
+ "is_avc": "true",
+ "nal_length_size": "4",
+ "r_frame_rate": "120/1",
+ "avg_frame_rate": "1704753000/71073479",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1421469580,
+ "duration": "15794.106444",
+ "bit_rate": "1147365",
+ "bits_per_raw_sample": "8",
+ "nb_frames": "378834",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "und",
+ "handler_name": "VideoHandler",
+ "vendor_id": "[0][0][0][0]"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_tag_string": "mp4a",
+ "codec_tag": "0x6134706d",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 7,
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/48000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 758115312,
+ "duration": "15794.069000",
+ "bit_rate": "224197",
+ "nb_frames": "740348",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "Surround 6.1",
+ "vendor_id": "[0][0][0][0]"
+ }
+ },
+ {
+ "index": 2,
+ "codec_name": "aac",
+ "codec_long_name": "AAC (Advanced Audio Coding)",
+ "profile": "LC",
+ "codec_type": "audio",
+ "codec_tag_string": "mp4a",
+ "codec_tag": "0x6134706d",
+ "sample_fmt": "fltp",
+ "sample_rate": "48000",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/48000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 758114304,
+ "duration": "15794.048000",
+ "bit_rate": "160519",
+ "nb_frames": "740347",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "Commentary",
+ "vendor_id": "[0][0][0][0]"
+ }
+ },
+ {
+ "index": 3,
+ "codec_name": "dvd_subtitle",
+ "codec_long_name": "DVD subtitles",
+ "codec_type": "subtitle",
+ "codec_tag_string": "mp4s",
+ "codec_tag": "0x7334706d",
+ "width": 720,
+ "height": 480,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1300301588,
+ "duration": "14447.795422",
+ "bit_rate": "2653",
+ "nb_frames": "3545",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "spa",
+ "handler_name": "SubtitleHandler"
+ }
+ },
+ {
+ "index": 4,
+ "codec_name": "mov_text",
+ "codec_long_name": "MOV text",
+ "codec_type": "subtitle",
+ "codec_tag_string": "tx3g",
+ "codec_tag": "0x67337874",
+ "width": 853,
+ "height": 51,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1401339330,
+ "duration": "15570.437000",
+ "bit_rate": "88",
+ "nb_frames": "5079",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "SubtitleHandler"
+ }
+ },
+ {
+ "index": 5,
+ "codec_name": "mov_text",
+ "codec_long_name": "MOV text",
+ "codec_type": "subtitle",
+ "codec_tag_string": "tx3g",
+ "codec_tag": "0x67337874",
+ "width": 853,
+ "height": 51,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/90000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration_ts": 1370580300,
+ "duration": "15228.670000",
+ "bit_rate": "18",
+ "nb_frames": "1563",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0
+ },
+ "tags": {
+ "creation_time": "2021-09-13T22:42:42.000000Z",
+ "language": "eng",
+ "handler_name": "Commentary"
+ }
+ }
+ ]
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt
new file mode 100644
index 000000000..b14aa8ea3
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example2.srt
@@ -0,0 +1,11 @@
+311
+00:16:46,465 --> 00:16:49,009
+Una vez que la gente se entere
+
+de que ustedes están aquí,
+
+312
+00:16:49,092 --> 00:16:51,470
+este lugar se convertirá
+
+en un maldito zoológico.
diff --git a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs
index e6c325bac..6948280a3 100644
--- a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs
+++ b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
-using MediaBrowser.Common.Cryptography;
+using MediaBrowser.Model.Cryptography;
using Xunit;
-namespace Jellyfin.Common.Tests.Cryptography
+namespace Jellyfin.Model.Tests.Cryptography
{
public static class PasswordHashTests
{
@@ -19,18 +19,16 @@ namespace Jellyfin.Common.Tests.Cryptography
Assert.Throws<ArgumentException>(() => new PasswordHash(string.Empty, Array.Empty<byte>()));
}
- public static IEnumerable<object[]> Parse_Valid_TestData()
+ public static TheoryData<string, PasswordHash> Parse_Valid_TestData()
{
+ var data = new TheoryData<string, PasswordHash>();
// Id
- yield return new object[]
- {
+ data.Add(
"$PBKDF2",
- new PasswordHash("PBKDF2", Array.Empty<byte>())
- };
+ new PasswordHash("PBKDF2", Array.Empty<byte>()));
// Id + parameter
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000",
new PasswordHash(
"PBKDF2",
@@ -39,12 +37,10 @@ namespace Jellyfin.Common.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" },
- })
- };
+ }));
// Id + parameters
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000,m=120",
new PasswordHash(
"PBKDF2",
@@ -54,34 +50,28 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
- })
- };
+ }));
// Id + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Array.Empty<byte>(),
- new Dictionary<string, string>())
- };
+ new Dictionary<string, string>()));
// Id + salt + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
Convert.FromHexString("62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"),
Convert.FromHexString("69F420"),
- new Dictionary<string, string>())
- };
+ new Dictionary<string, string>()));
// Id + parameter + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -90,12 +80,9 @@ namespace Jellyfin.Common.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" }
- })
- };
-
+ }));
// Id + parameters + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000,m=120$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -105,12 +92,9 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
- })
- };
-
+ }));
// Id + parameters + salt + hash
- yield return new object[]
- {
+ data.Add(
"$PBKDF2$iterations=1000,m=120$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D",
new PasswordHash(
"PBKDF2",
@@ -120,8 +104,8 @@ namespace Jellyfin.Common.Tests.Cryptography
{
{ "iterations", "1000" },
{ "m", "120" }
- })
- };
+ }));
+ return data;
}
[Theory]
@@ -171,11 +155,11 @@ namespace Jellyfin.Common.Tests.Cryptography
[InlineData("$PBKDF2$=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
[InlineData("$PBKDF2$=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
[InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
- [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Ends on $
- [InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Extra segment
- [InlineData("$PBKDF2$iterations=$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$anotherone")] // Extra segment
- [InlineData("$PBKDF2$iterations=$invalidstalt$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid salt
- [InlineData("$PBKDF2$iterations=$69F420$invalid hash")] // Invalid hash
+ [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Ends on $
+ [InlineData("$PBKDF2$iterations=1000$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Extra segment
+ [InlineData("$PBKDF2$iterations=1000$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$anotherone")] // Extra segment
+ [InlineData("$PBKDF2$iterations=1000$invalidstalt$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid salt
+ [InlineData("$PBKDF2$iterations=1000$69F420$invalid hash")] // Invalid hash
[InlineData("$PBKDF2$69F420$")] // Empty hash
public static void Parse_InvalidFormat_ThrowsFormatException(string passwordHash)
{
diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
new file mode 100644
index 000000000..0c97a90b4
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
@@ -0,0 +1,150 @@
+using MediaBrowser.Model.Entities;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Entities
+{
+ public class MediaStreamTests
+ {
+ public static TheoryData<MediaStream, string> Get_DisplayTitle_TestData()
+ {
+ var data = new TheoryData<MediaStream, string>();
+
+ data.Add(
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = string.Empty,
+ IsForced = false,
+ IsDefault = false,
+ Codec = "ASS"
+ },
+ "English - Und - ASS");
+
+ data.Add(
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = string.Empty,
+ IsForced = false,
+ IsDefault = false,
+ Codec = string.Empty
+ },
+ "English - Und");
+
+ data.Add(
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = "EN",
+ IsForced = false,
+ IsDefault = false,
+ Codec = string.Empty
+ },
+ "English");
+
+ data.Add(
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = "English",
+ Language = "EN",
+ IsForced = true,
+ IsDefault = true,
+ Codec = "SRT"
+ },
+ "English - Default - Forced - SRT");
+
+ data.Add(
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = null,
+ Language = null,
+ IsForced = false,
+ IsDefault = false,
+ Codec = null
+ },
+ "Und");
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(Get_DisplayTitle_TestData))]
+ public void Get_DisplayTitle_should_return_valid_title(MediaStream mediaStream, string expected)
+ {
+ Assert.Equal(expected, mediaStream.DisplayTitle);
+ }
+
+ [Theory]
+ [InlineData(null, null, false, null)]
+ [InlineData(null, 0, false, null)]
+ [InlineData(0, null, false, null)]
+ [InlineData(640, 480, false, "480p")]
+ [InlineData(640, 480, true, "480i")]
+ [InlineData(720, 576, false, "576p")]
+ [InlineData(720, 576, true, "576i")]
+ [InlineData(960, 540, false, "540p")]
+ [InlineData(960, 540, true, "540i")]
+ [InlineData(1280, 720, false, "720p")]
+ [InlineData(1280, 720, true, "720i")]
+ [InlineData(1920, 1080, false, "1080p")]
+ [InlineData(1920, 1080, true, "1080i")]
+ [InlineData(4096, 3072, false, "4K")]
+ [InlineData(8192, 6144, false, "8K")]
+ [InlineData(512, 384, false, "480p")]
+ [InlineData(576, 336, false, "480p")]
+ [InlineData(624, 352, false, "480p")]
+ [InlineData(640, 352, false, "480p")]
+ [InlineData(704, 396, false, "480p")]
+ [InlineData(720, 404, false, "480p")]
+ [InlineData(720, 480, false, "480p")]
+ [InlineData(768, 576, false, "576p")]
+ [InlineData(960, 720, false, "720p")]
+ [InlineData(1280, 528, false, "720p")]
+ [InlineData(1280, 532, false, "720p")]
+ [InlineData(1280, 534, false, "720p")]
+ [InlineData(1280, 536, false, "720p")]
+ [InlineData(1280, 544, false, "720p")]
+ [InlineData(1280, 690, false, "720p")]
+ [InlineData(1280, 694, false, "720p")]
+ [InlineData(1280, 696, false, "720p")]
+ [InlineData(1280, 716, false, "720p")]
+ [InlineData(1280, 718, false, "720p")]
+ [InlineData(1912, 792, false, "1080p")]
+ [InlineData(1916, 1076, false, "1080p")]
+ [InlineData(1918, 1080, false, "1080p")]
+ [InlineData(1920, 796, false, "1080p")]
+ [InlineData(1920, 800, false, "1080p")]
+ [InlineData(1920, 802, false, "1080p")]
+ [InlineData(1920, 804, false, "1080p")]
+ [InlineData(1920, 808, false, "1080p")]
+ [InlineData(1920, 816, false, "1080p")]
+ [InlineData(1920, 856, false, "1080p")]
+ [InlineData(1920, 960, false, "1080p")]
+ [InlineData(1920, 1024, false, "1080p")]
+ [InlineData(1920, 1040, false, "1080p")]
+ [InlineData(1920, 1072, false, "1080p")]
+ [InlineData(1440, 1072, false, "1080p")]
+ [InlineData(1440, 1080, false, "1080p")]
+ [InlineData(3840, 1600, false, "4K")]
+ [InlineData(3840, 1606, false, "4K")]
+ [InlineData(3840, 1608, false, "4K")]
+ [InlineData(3840, 2160, false, "4K")]
+ [InlineData(7680, 4320, false, "8K")]
+ public void GetResolutionText_Valid(int? width, int? height, bool interlaced, string expected)
+ {
+ var mediaStream = new MediaStream()
+ {
+ Width = width,
+ Height = height,
+ IsInterlaced = interlaced
+ };
+
+ Assert.Equal(expected, mediaStream.GetResolutionText());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 40c51e524..7e8397d9f 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -1,20 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
- <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
new file mode 100644
index 000000000..cbab455f0
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
@@ -0,0 +1,159 @@
+using MediaBrowser.Model.Net;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Net
+{
+ public class MimeTypesTests
+ {
+ [Theory]
+ [InlineData(".dll", "application/octet-stream")]
+ [InlineData(".log", "text/plain")]
+ [InlineData(".srt", "application/x-subrip")]
+ [InlineData(".html", "text/html; charset=UTF-8")]
+ [InlineData(".htm", "text/html; charset=UTF-8")]
+ [InlineData(".7z", "application/x-7z-compressed")]
+ [InlineData(".azw", "application/vnd.amazon.ebook")]
+ [InlineData(".azw3", "application/vnd.amazon.ebook")]
+ [InlineData(".eot", "application/vnd.ms-fontobject")]
+ [InlineData(".epub", "application/epub+zip")]
+ [InlineData(".json", "application/json")]
+ [InlineData(".mobi", "application/x-mobipocket-ebook")]
+ [InlineData(".opf", "application/oebps-package+xml")]
+ [InlineData(".pdf", "application/pdf")]
+ [InlineData(".rar", "application/vnd.rar")]
+ [InlineData(".ttml", "application/ttml+xml")]
+ [InlineData(".wasm", "application/wasm")]
+ [InlineData(".xml", "application/xml")]
+ [InlineData(".zip", "application/zip")]
+ [InlineData(".bmp", "image/bmp")]
+ [InlineData(".gif", "image/gif")]
+ [InlineData(".ico", "image/vnd.microsoft.icon")]
+ [InlineData(".jpg", "image/jpeg")]
+ [InlineData(".jpeg", "image/jpeg")]
+ [InlineData(".png", "image/png")]
+ [InlineData(".svg", "image/svg+xml")]
+ [InlineData(".svgz", "image/svg+xml")]
+ [InlineData(".tbn", "image/jpeg")]
+ [InlineData(".tif", "image/tiff")]
+ [InlineData(".tiff", "image/tiff")]
+ [InlineData(".webp", "image/webp")]
+ [InlineData(".ttf", "font/ttf")]
+ [InlineData(".woff", "font/woff")]
+ [InlineData(".woff2", "font/woff2")]
+ [InlineData(".ass", "text/x-ssa")]
+ [InlineData(".ssa", "text/x-ssa")]
+ [InlineData(".css", "text/css")]
+ [InlineData(".csv", "text/csv")]
+ [InlineData(".edl", "text/plain")]
+ [InlineData(".txt", "text/plain")]
+ [InlineData(".vtt", "text/vtt")]
+ [InlineData(".3gp", "video/3gpp")]
+ [InlineData(".3g2", "video/3gpp2")]
+ [InlineData(".asf", "video/x-ms-asf")]
+ [InlineData(".avi", "video/x-msvideo")]
+ [InlineData(".flv", "video/x-flv")]
+ [InlineData(".mp4", "video/mp4")]
+ [InlineData(".m4v", "video/x-m4v")]
+ [InlineData(".mpegts", "video/mp2t")]
+ [InlineData(".mpg", "video/mpeg")]
+ [InlineData(".mkv", "video/x-matroska")]
+ [InlineData(".mov", "video/quicktime")]
+ [InlineData(".ogv", "video/ogg")]
+ [InlineData(".ts", "video/mp2t")]
+ [InlineData(".webm", "video/webm")]
+ [InlineData(".wmv", "video/x-ms-wmv")]
+ [InlineData(".aac", "audio/aac")]
+ [InlineData(".ac3", "audio/ac3")]
+ [InlineData(".ape", "audio/x-ape")]
+ [InlineData(".dsf", "audio/dsf")]
+ [InlineData(".dsp", "audio/dsp")]
+ [InlineData(".flac", "audio/flac")]
+ [InlineData(".m4a", "audio/mp4")]
+ [InlineData(".m4b", "audio/m4b")]
+ [InlineData(".mid", "audio/midi")]
+ [InlineData(".midi", "audio/midi")]
+ [InlineData(".mp3", "audio/mpeg")]
+ [InlineData(".oga", "audio/ogg")]
+ [InlineData(".ogg", "audio/ogg")]
+ [InlineData(".opus", "audio/ogg")]
+ [InlineData(".vorbis", "audio/vorbis")]
+ [InlineData(".wav", "audio/wav")]
+ [InlineData(".webma", "audio/webm")]
+ [InlineData(".wma", "audio/x-ms-wma")]
+ [InlineData(".wv", "audio/x-wavpack")]
+ [InlineData(".xsp", "audio/xsp")]
+ public void GetMimeType_Valid_ReturnsCorrectResult(string input, string expectedResult)
+ {
+ Assert.Equal(expectedResult, MimeTypes.GetMimeType(input, null));
+ }
+
+ [Theory]
+ [InlineData("application/epub+zip", ".epub")]
+ [InlineData("application/json", ".json")]
+ [InlineData("application/oebps-package+xml", ".opf")]
+ [InlineData("application/pdf", ".pdf")]
+ [InlineData("application/ttml+xml", ".ttml")]
+ [InlineData("application/vnd.amazon.ebook", ".azw")]
+ [InlineData("application/vnd.ms-fontobject", ".eot")]
+ [InlineData("application/vnd.rar", ".rar")]
+ [InlineData("application/wasm", ".wasm")]
+ [InlineData("application/x-7z-compressed", ".7z")]
+ [InlineData("application/x-cbz", ".cbz")]
+ [InlineData("application/x-javascript", ".js")]
+ [InlineData("application/x-mobipocket-ebook", ".mobi")]
+ [InlineData("application/x-mpegURL", ".m3u8")]
+ [InlineData("application/x-subrip", ".srt")]
+ [InlineData("application/xml", ".xml")]
+ [InlineData("application/zip", ".zip")]
+ [InlineData("audio/aac", ".aac")]
+ [InlineData("audio/ac3", ".ac3")]
+ [InlineData("audio/dsf", ".dsf")]
+ [InlineData("audio/dsp", ".dsp")]
+ [InlineData("audio/flac", ".flac")]
+ [InlineData("audio/m4b", ".m4b")]
+ [InlineData("audio/mp4", ".m4a")]
+ [InlineData("audio/vorbis", ".vorbis")]
+ [InlineData("audio/wav", ".wav")]
+ [InlineData("audio/x-aac", ".aac")]
+ [InlineData("audio/x-ape", ".ape")]
+ [InlineData("audio/x-ms-wma", ".wma")]
+ [InlineData("audio/x-wavpack", ".wv")]
+ [InlineData("audio/xsp", ".xsp")]
+ [InlineData("font/ttf", ".ttf")]
+ [InlineData("font/woff", ".woff")]
+ [InlineData("font/woff2", ".woff2")]
+ [InlineData("image/bmp", ".bmp")]
+ [InlineData("image/gif", ".gif")]
+ [InlineData("image/jpeg", ".jpg")]
+ [InlineData("image/png", ".png")]
+ [InlineData("image/svg+xml", ".svg")]
+ [InlineData("image/tiff", ".tif")]
+ [InlineData("image/vnd.microsoft.icon", ".ico")]
+ [InlineData("image/webp", ".webp")]
+ [InlineData("image/x-png", ".png")]
+ [InlineData("text/css", ".css")]
+ [InlineData("text/csv", ".csv")]
+ [InlineData("text/plain", ".txt")]
+ [InlineData("text/rtf", ".rtf")]
+ [InlineData("text/vtt", ".vtt")]
+ [InlineData("text/x-ssa", ".ssa")]
+ [InlineData("video/3gpp", ".3gp")]
+ [InlineData("video/3gpp2", ".3g2")]
+ [InlineData("video/mp2t", ".ts")]
+ [InlineData("video/mp4", ".mp4")]
+ [InlineData("video/ogg", ".ogv")]
+ [InlineData("video/quicktime", ".mov")]
+ [InlineData("video/vnd.mpeg.dash.mpd", ".mpd")]
+ [InlineData("video/webm", ".webm")]
+ [InlineData("video/x-flv", ".flv")]
+ [InlineData("video/x-m4v", ".m4v")]
+ [InlineData("video/x-matroska", ".mkv")]
+ [InlineData("video/x-ms-asf", ".asf")]
+ [InlineData("video/x-ms-wmv", ".wmv")]
+ [InlineData("video/x-msvideo", ".avi")]
+ public void ToExtension_Valid_ReturnsCorrectResult(string input, string expectedResult)
+ {
+ Assert.Equal(expectedResult, MimeTypes.ToExtension(input));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
index 53b35c2d6..c72a3315e 100644
--- a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Xunit;
@@ -9,29 +8,29 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
private readonly NamingOptions _namingOptions = new NamingOptions();
- public static IEnumerable<object[]> Resolve_ValidFileNameTestData()
+ public static TheoryData<AudioBookFileInfo> Resolve_ValidFileNameTestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<AudioBookFileInfo>();
+
+ data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
- "mp3")
- };
- yield return new object[]
- {
+ "mp3"));
+
+ data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
"ogg",
- chapterNumber: 1)
- };
- yield return new object[]
- {
+ chapterNumber: 1));
+
+ data.Add(
new AudioBookFileInfo(
@"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
"mp3",
chapterNumber: 2,
- partNumber: 3)
- };
+ partNumber: 3));
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
index 3892d00f6..58aaed023 100644
--- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
+++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs
@@ -10,7 +10,6 @@ namespace Jellyfin.Naming.Tests.Common
{
var options = new NamingOptions();
- Assert.NotEmpty(options.VideoFileStackingRegexes);
Assert.NotEmpty(options.CleanDateTimeRegexes);
Assert.NotEmpty(options.CleanStringRegexes);
Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes);
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index e386cb8c1..4096873a3 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -6,19 +6,16 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
index 921c2b1f5..1e7fedb36 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
@@ -70,9 +70,10 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Log Horizon 2/[HorribleSubs] Log Horizon 2 - 03 [720p].mkv", 3)] // digit in series name
[InlineData("Season 1/seriesname 05.mkv", 5)] // no hyphen between series name and episode number
[InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number
- // TODO: [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
+ [InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number
+ [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
+ // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
- // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)]
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
// TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)]
// TODO: [InlineData("Season 02/02x03x04x15 - Ep Name.mp4", 2)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs
new file mode 100644
index 000000000..ceb5f8b73
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeriesPathParserTest.cs
@@ -0,0 +1,28 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SeriesPathParserTest
+ {
+ [Theory]
+ [InlineData("The.Show.S01", "The.Show")]
+ [InlineData("/The.Show.S01", "The.Show")]
+ [InlineData("/some/place/The.Show.S01", "The.Show")]
+ [InlineData("/something/The.Show.S01", "The.Show")]
+ [InlineData("The Show Season 10", "The Show")]
+ [InlineData("The Show S01E01", "The Show")]
+ [InlineData("The Show S01E01 Episode", "The Show")]
+ [InlineData("/something/The Show/Season 1", "The Show")]
+ [InlineData("/something/The Show/S01", "The Show")]
+ public void SeriesPathParserParseTest(string path, string name)
+ {
+ NamingOptions o = new NamingOptions();
+ var res = SeriesPathParser.Parse(o, path);
+
+ Assert.Equal(name, res.SeriesName);
+ Assert.True(res.Success);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
new file mode 100644
index 000000000..97f4b4058
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
@@ -0,0 +1,28 @@
+using Emby.Naming.Common;
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV
+{
+ public class SeriesResolverTests
+ {
+ [Theory]
+ [InlineData("The.Show.S01", "The Show")]
+ [InlineData("The.Show.S01.COMPLETE", "The Show")]
+ [InlineData("S.H.O.W.S01", "S.H.O.W")]
+ [InlineData("The.Show.P.I.S01", "The Show P.I")]
+ [InlineData("The_Show_Season_1", "The Show")]
+ [InlineData("/something/The_Show/Season 10", "The Show")]
+ [InlineData("The Show", "The Show")]
+ [InlineData("/some/path/The Show", "The Show")]
+ [InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")]
+ [InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")]
+ public void SeriesResolverResolveTest(string path, string name)
+ {
+ NamingOptions o = new NamingOptions();
+ var res = SeriesResolver.Resolve(o, path);
+
+ Assert.Equal(name, res.Name);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
index 89579c037..6d49ac832 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs
@@ -21,7 +21,8 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("[Baz-Bar]Foo - [1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
[InlineData(@"/Foo/The.Series.Name.S01E04.WEBRip.x264-Baz[Bar]/the.series.name.s01e04.webrip.x264-Baz[Bar].mkv", "The.Series.Name", 1, 4)]
[InlineData(@"Love.Death.and.Robots.S01.1080p.NF.WEB-DL.DDP5.1.x264-NTG/Love.Death.and.Robots.S01E01.Sonnies.Edge.1080p.NF.WEB-DL.DDP5.1.x264-NTG.mkv", "Love.Death.and.Robots", 1, 1)]
- // TODO: [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
+ [InlineData("[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken/[YuiSubs] Tensura Nikki - Tensei Shitara Slime Datta Ken - 12 (NVENC H.265 1080p).mkv", "Tensura Nikki - Tensei Shitara Slime Datta Ken", null, 12)]
+ [InlineData("[Baz-Bar]Foo - 01 - 12[1080p][Multiple Subtitle]/[Baz-Bar] Foo - 05 [1080p][Multiple Subtitle].mkv", "Foo", null, 5)]
// TODO: [InlineData("E:\\Anime\\Yahari Ore no Seishun Love Comedy wa Machigatteiru\\Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku\\Oregairu Zoku 11 - Hayama Hayato Always Renconds to Everyone's Expectations..mkv", "Yahari Ore no Seishun Love Comedy wa Machigatteiru", null, 11)]
// TODO: [InlineData(@"/Library/Series/The Grand Tour (2016)/Season 1/S01E01 The Holy Trinity.mkv", "The Grand Tour", 1, 1)]
public void TestSimple(string path, string seriesName, int? seasonNumber, int? episodeNumber)
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index fb050cf5a..1574bce58 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -1,4 +1,3 @@
-using System;
using Emby.Naming.Common;
using Emby.Naming.Video;
using Xunit;
@@ -23,12 +22,17 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
+ [InlineData("[HorribleSubs] Made in Abyss - 13 [720p].mkv", "Made in Abyss")]
+ [InlineData("[Tsundere] Kore wa Zombie Desu ka of the Dead [BDRip h264 1920x1080 FLAC]", "Kore wa Zombie Desu ka of the Dead")]
+ [InlineData("[Erai-raws] Jujutsu Kaisen - 03 [720p][Multiple Subtitle].mkv", "Jujutsu Kaisen")]
+ [InlineData("[OCN] 애타는 로맨스 720p-NEXT", "애타는 로맨스")]
+ [InlineData("[tvN] 혼술남녀.E01-E16.720p-NEXT", "혼술남녀")]
+ [InlineData("[tvN] 연애말고 결혼 E01~E16 END HDTV.H264.720p-WITH", "연애말고 결혼")]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
- Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
- // TODO: compare spans when XUnit supports it
- Assert.Equal(expectedName, newName.ToString());
+ Assert.True(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
+ Assert.Equal(expectedName, newName);
}
[Theory]
@@ -41,8 +45,8 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Run lola run (lola rennt) (2009).mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
- Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out ReadOnlySpan<char> newName));
- Assert.True(newName.IsEmpty);
+ Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
+ Assert.True(string.IsNullOrEmpty(newName));
}
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index f872f94f8..8dd637559 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -18,30 +18,31 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestKodiExtras()
{
- Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
- Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
+ Test("trailer.mp4", ExtraType.Trailer);
+ Test("300-trailer.mp4", ExtraType.Trailer);
- Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
+ Test("theme.mp3", ExtraType.ThemeSong);
}
[Fact]
public void TestExpandedExtras()
{
- Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
- Test("trailer.mp3", null, _videoOptions);
- Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
-
- Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
- Test("theme.mkv", null, _videoOptions);
-
- Test("300-scene.mp4", ExtraType.Scene, _videoOptions);
- Test("300-scene2.mp4", ExtraType.Scene, _videoOptions);
- Test("300-clip.mp4", ExtraType.Clip, _videoOptions);
-
- Test("300-deleted.mp4", ExtraType.DeletedScene, _videoOptions);
- Test("300-deletedscene.mp4", ExtraType.DeletedScene, _videoOptions);
- Test("300-interview.mp4", ExtraType.Interview, _videoOptions);
- Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, _videoOptions);
+ Test("trailer.mp4", ExtraType.Trailer);
+ Test("trailer.mp3", null);
+ Test("300-trailer.mp4", ExtraType.Trailer);
+ Test("stuff trailerthings.mkv", null);
+
+ Test("theme.mp3", ExtraType.ThemeSong);
+ Test("theme.mkv", null);
+
+ Test("300-scene.mp4", ExtraType.Scene);
+ Test("300-scene2.mp4", ExtraType.Scene);
+ Test("300-clip.mp4", ExtraType.Clip);
+
+ Test("300-deleted.mp4", ExtraType.DeletedScene);
+ Test("300-deletedscene.mp4", ExtraType.DeletedScene);
+ Test("300-interview.mp4", ExtraType.Interview);
+ Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes);
}
[Theory]
@@ -55,9 +56,9 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData(ExtraType.Unknown, "extras")]
public void TestDirectories(ExtraType type, string dirName)
{
- Test(dirName + "/300.mp4", type, _videoOptions);
- Test("300/" + dirName + "/something.mkv", type, _videoOptions);
- Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type, _videoOptions);
+ Test(dirName + "/300.mp4", type);
+ Test("300/" + dirName + "/something.mkv", type);
+ Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type);
}
[Theory]
@@ -66,32 +67,23 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("The Big Short")]
public void TestNonExtraDirectories(string dirName)
{
- Test(dirName + "/300.mp4", null, _videoOptions);
- Test("300/" + dirName + "/something.mkv", null, _videoOptions);
- Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null, _videoOptions);
- Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null, _videoOptions);
+ Test(dirName + "/300.mp4", null);
+ Test("300/" + dirName + "/something.mkv", null);
+ Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null);
+ Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null);
}
[Fact]
public void TestSample()
{
- Test("300-sample.mp4", ExtraType.Sample, _videoOptions);
+ Test("300-sample.mp4", ExtraType.Sample);
}
- private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)
+ private void Test(string input, ExtraType? expectedType)
{
- var parser = GetExtraTypeParser(videoOptions);
-
- var extraType = parser.GetExtraInfo(input).ExtraType;
-
- if (expectedType == null)
- {
- Assert.Null(extraType);
- }
- else
- {
- Assert.Equal(expectedType, extraType);
- }
+ var extraType = ExtraResolver.GetExtraInfo(input, _videoOptions).ExtraType;
+
+ Assert.Equal(expectedType, extraType);
}
[Fact]
@@ -99,14 +91,9 @@ namespace Jellyfin.Naming.Tests.Video
{
var rule = new ExtraRule(ExtraType.Unknown, ExtraRuleType.Regex, @"([eE]x(tra)?\.\w+)", MediaType.Video);
var options = new NamingOptions { VideoExtraRules = new[] { rule } };
- var res = GetExtraTypeParser(options).GetExtraInfo("extra.mp4");
+ var res = ExtraResolver.GetExtraInfo("extra.mp4", options);
Assert.Equal(rule, res.Rule);
}
-
- private ExtraResolver GetExtraTypeParser(NamingOptions videoOptions)
- {
- return new ExtraResolver(videoOptions);
- }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index d02f8ae92..9a9a57be4 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -23,15 +23,11 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
- Assert.Single(result[0].Extras);
+ Assert.Single(result.Where(v => v.ExtraType == null));
+ Assert.Single(result.Where(v => v.ExtraType != null));
}
[Fact]
@@ -46,15 +42,11 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
- Assert.Single(result[0].Extras);
+ Assert.Single(result.Where(v => v.ExtraType == null));
+ Assert.Single(result.Where(v => v.ExtraType != null));
Assert.Equal(2, result[0].AlternateVersions.Count);
}
@@ -68,11 +60,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
@@ -94,15 +82,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(7, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -122,15 +105,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
}
@@ -151,15 +129,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(9, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -176,15 +149,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -203,15 +171,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -231,15 +194,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[2].Is3D);
Assert.True(result[0].AlternateVersions[3].Is3D);
@@ -262,15 +220,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Equal(7, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[3].Is3D);
Assert.True(result[0].AlternateVersions[4].Is3D);
@@ -287,11 +240,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -312,15 +261,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(7, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -339,15 +283,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
- Assert.Empty(result[0].Extras);
Assert.Empty(result[0].AlternateVersions);
}
@@ -361,15 +300,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@@ -383,15 +317,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@@ -405,15 +334,10 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
- Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
@@ -427,11 +351,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -440,7 +360,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestEmptyList()
{
- var result = VideoListResolver.Resolve(new List<FileSystemMetadata>(), _namingOptions).ToList();
+ var result = VideoListResolver.Resolve(new List<VideoFileInfo>(), _namingOptions).ToList();
Assert.Empty(result);
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
index 8794d3ebe..368c3592e 100644
--- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs
@@ -22,9 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006)", 4);
@@ -39,9 +37,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2007).mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -55,9 +51,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys 2007.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -71,9 +65,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2007).mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -87,9 +79,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 2007.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -103,9 +93,7 @@ namespace Jellyfin.Naming.Tests.Video
"Star Trek 2- The wrath of khan.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -119,9 +107,7 @@ namespace Jellyfin.Naming.Tests.Video
"Red Riding in the Year of Our Lord 1974 (2009).mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -135,16 +121,14 @@ namespace Jellyfin.Naming.Tests.Video
"d:/movies/300 2006 part2.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "300 2006", 2);
}
[Fact]
- public void TestDirtyNames()
+ public void ResolveFiles_GivenPartInMiddleOfName_ReturnsNoStack()
{
var files = new[]
{
@@ -155,16 +139,13 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
- Assert.Single(result);
- TestStackInfo(result[0], "Bad Boys (2006).stv.unrated.multi.1080p.bluray.x264-rough", 4);
+ Assert.Empty(result);
}
[Fact]
- public void TestNumberedFiles()
+ public void ResolveFiles_FileNamesWithMissingPartType_ReturnsNoStack()
{
var files = new[]
{
@@ -175,9 +156,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -194,9 +173,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "300 (2006)", 4);
@@ -214,9 +191,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "Bad Boys (2006)", 3);
@@ -238,9 +213,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
TestStackInfo(result[1], "Bad Boys (2006)", 4);
@@ -256,9 +229,7 @@ namespace Jellyfin.Naming.Tests.Video
"blah blah - cd 2"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveDirectories(files).ToList();
+ var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
Assert.Single(result);
TestStackInfo(result[0], "blah blah", 2);
@@ -275,9 +246,7 @@ namespace Jellyfin.Naming.Tests.Video
"300-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
@@ -297,9 +266,7 @@ namespace Jellyfin.Naming.Tests.Video
"Avengers part3.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -328,9 +295,7 @@ namespace Jellyfin.Naming.Tests.Video
"300-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Equal(3, result.Count);
@@ -354,9 +319,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006)-trailer.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
@@ -375,9 +338,7 @@ namespace Jellyfin.Naming.Tests.Video
new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true }
};
- var resolver = GetResolver();
-
- var result = resolver.Resolve(files).ToList();
+ var result = StackResolver.Resolve(files, _namingOptions).ToList();
Assert.Equal(2, result.Count);
TestStackInfo(result[0], "300 (2006)", 3);
@@ -397,9 +358,7 @@ namespace Jellyfin.Naming.Tests.Video
"Harry Potter and the Deathly Hallows 4.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Empty(result);
}
@@ -414,9 +373,7 @@ namespace Jellyfin.Naming.Tests.Video
"Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveFiles(files).ToList();
+ var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Files.Count);
@@ -432,9 +389,7 @@ namespace Jellyfin.Naming.Tests.Video
@"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
};
- var resolver = GetResolver();
-
- var result = resolver.ResolveDirectories(files).ToList();
+ var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Files.Count);
@@ -445,10 +400,5 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(fileCount, stack.Files.Count);
Assert.Equal(name, stack.Name);
}
-
- private StackResolver GetResolver()
- {
- return new StackResolver(_namingOptions);
- }
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index 9e0776c3c..b76187842 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Xunit;
@@ -41,23 +42,28 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Equal(5, result.Count);
+ Assert.Equal(11, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
Assert.NotNull(batman);
Assert.Equal(3, batman!.Files.Count);
- Assert.Equal(3, batman!.Extras.Count);
var harry = result.FirstOrDefault(x => string.Equals(x.Name, "Harry Potter and the Deathly Hallows", StringComparison.Ordinal));
Assert.NotNull(harry);
Assert.Equal(4, harry!.Files.Count);
- Assert.Equal(2, harry!.Extras.Count);
+
+ Assert.False(result[2].ExtraType.HasValue);
+
+ Assert.Equal(ExtraType.Trailer, result[3].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[4].ExtraType);
+ Assert.Equal(ExtraType.DeletedScene, result[5].ExtraType);
+ Assert.Equal(ExtraType.Sample, result[6].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[7].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[8].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[9].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[10].ExtraType);
}
[Fact]
@@ -70,11 +76,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
@@ -90,14 +92,12 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(2, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -110,14 +110,12 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(2, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -131,34 +129,51 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(3, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
- public void TestDifferentNames()
+ public void Resolve_SameNameAndYear_ReturnsSingleItem()
{
var files = new[]
{
"Looper (2012)-trailer.mkv",
+ "Looper 2012-trailer.mkv",
"Looper.2012.bluray.720p.x264.mkv"
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(3, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
+ }
+
+ [Fact]
+ public void Resolve_TrailerMatchesFolderName_ReturnsSingleItem()
+ {
+ var files = new[]
+ {
+ "/movies/Looper (2012)/Looper (2012)-trailer.mkv",
+ "/movies/Looper (2012)/Looper.bluray.720p.x264.mkv"
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ _namingOptions).ToList();
+
+ Assert.Equal(2, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -175,11 +190,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(5, result.Count);
@@ -195,11 +206,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
@@ -216,11 +223,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = true,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -233,39 +236,18 @@ namespace Jellyfin.Naming.Tests.Video
{
@"No (2012) part1.mp4",
@"No (2012) part2.mp4",
- @"No (2012) part1-trailer.mp4"
- };
-
- var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
- _namingOptions).ToList();
-
- Assert.Single(result);
- }
-
- [Fact]
- public void TestStackedWithTrailer2()
- {
- var files = new[]
- {
- @"No (2012) part1.mp4",
- @"No (2012) part2.mp4",
+ @"No (2012) part1-trailer.mp4",
@"No (2012)-trailer.mp4"
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(3, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
}
[Fact]
@@ -276,18 +258,18 @@ namespace Jellyfin.Naming.Tests.Video
@"/Movies/Top Gun (1984)/movie.mp4",
@"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
@"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
- @"trailer.mp4"
+ @"/Movies/trailer.mp4"
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(4, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[2].ExtraType);
+ Assert.Equal(ExtraType.Trailer, result[3].ExtraType);
}
[Fact]
@@ -302,11 +284,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -321,11 +299,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
@@ -340,11 +314,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
@@ -360,11 +330,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Single(result);
@@ -380,11 +346,7 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
Assert.Equal(2, result.Count);
@@ -396,40 +358,34 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
@"/Server/Despicable Me/Despicable Me (2010).mkv",
- @"/Server/Despicable Me/movie-trailer.mkv"
+ @"/Server/Despicable Me/trailer.mkv"
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(2, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
- public void TestTrailerFalsePositives()
+ public void Resolve_TrailerInTrailersFolder_ReturnsCorrectExtraType()
{
var files = new[]
{
- @"/Server/Despicable Me/Skyscraper (2018) - Big Game Spot.mkv",
- @"/Server/Despicable Me/Skyscraper (2018) - Trailer.mkv",
- @"/Server/Despicable Me/Baywatch (2017) - Big Game Spot.mkv",
- @"/Server/Despicable Me/Baywatch (2017) - Trailer.mkv"
+ @"/Server/Despicable Me/Despicable Me (2010).mkv",
+ @"/Server/Despicable Me/trailers/some title.mkv"
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Equal(4, result.Count);
+ Assert.Equal(2, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
@@ -442,20 +398,18 @@ namespace Jellyfin.Naming.Tests.Video
};
var result = VideoListResolver.Resolve(
- files.Select(i => new FileSystemMetadata
- {
- IsDirectory = false,
- FullName = i
- }).ToList(),
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Single(result);
+ Assert.Equal(2, result.Count);
+ Assert.False(result[0].ExtraType.HasValue);
+ Assert.Equal(ExtraType.Trailer, result[1].ExtraType);
}
[Fact]
public void TestDirectoryStack()
{
- var stack = new FileStack();
+ var stack = new FileStack(string.Empty, false, Array.Empty<string>());
Assert.False(stack.ContainsFile("XX", true));
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
index ac5a7a21e..33a99e107 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
@@ -11,148 +10,134 @@ namespace Jellyfin.Naming.Tests.Video
{
private static NamingOptions _namingOptions = new NamingOptions();
- public static IEnumerable<object[]> ResolveFile_ValidFileNameTestData()
+ public static TheoryData<VideoFileInfo> ResolveFile_ValidFileNameTestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<VideoFileInfo>();
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/7 Psychos.mkv/7 Psychos.mkv",
container: "mkv",
- name: "7 Psychos")
- };
- yield return new object[]
- {
+ name: "7 Psychos"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/3 days to kill (2005)/3 days to kill (2005).mkv",
container: "mkv",
name: "3 days to kill",
- year: 2005)
- };
- yield return new object[]
- {
+ year: 2005));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/American Psycho/American.Psycho.mkv",
container: "mkv",
- name: "American.Psycho")
- };
- yield return new object[]
- {
+ name: "American.Psycho"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006).3d.sbs.mkv",
container: "mkv",
name: "brave",
year: 2006,
is3D: true,
- format3D: "sbs")
- };
- yield return new object[]
- {
+ format3D: "sbs"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d1.sbas.mkv",
container: "mkv",
name: "300",
- year: 2006)
- };
- yield return new object[]
- {
+ year: 2006));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).3d.sbs.mkv",
container: "mkv",
name: "300",
year: 2006,
is3D: true,
- format3D: "sbs")
- };
- yield return new object[]
- {
+ format3D: "sbs"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/brave (2007)/brave (2006)-trailer.bluray.disc",
container: "disc",
name: "brave",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).bluray.disc",
container: "disc",
name: "Brave",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).bluray.disc",
container: "disc",
name: "300",
year: 2006,
isStub: true,
- stubType: "bluray")
- };
- yield return new object[]
- {
+ stubType: "bluray"));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006)-trailer.mkv",
container: "mkv",
name: "300",
year: 2006,
- extraType: ExtraType.Trailer)
- };
- yield return new object[]
- {
+ extraType: ExtraType.Trailer));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006)-trailer.mkv",
container: "mkv",
name: "Brave",
year: 2006,
- extraType: ExtraType.Trailer)
- };
- yield return new object[]
- {
+ extraType: ExtraType.Trailer));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/300 (2007)/300 (2006).mkv",
container: "mkv",
name: "300",
- year: 2006)
- };
- yield return new object[]
- {
+ year: 2006));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Bad Boys (1995)/Bad Boys (1995).mkv",
container: "mkv",
name: "Bad Boys",
- year: 1995)
- };
- yield return new object[]
- {
+ year: 1995));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Brave (2007)/Brave (2006).mkv",
container: "mkv",
name: "Brave",
- year: 2006)
- };
- yield return new object[]
- {
+ year: 2006));
+
+ data.Add(
new VideoFileInfo(
path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - JEFF.mp4",
container: "mp4",
name: "Rain Man",
- year: 1988)
- };
+ year: 1988));
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 97bf673ae..78556ee67 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -6,20 +6,17 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
- <PackageReference Include="FsCheck.Xunit" Version="2.15.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
index 1cad625b7..61f913252 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs
@@ -34,7 +34,7 @@ namespace Jellyfin.Networking.Tests
}
/// <summary>
- /// Checks that thge given IP address is not in the network provided.
+ /// Checks that the given IP address is not in the network provided.
/// </summary>
/// <param name="network">Network address(es).</param>
/// <param name="value">The IP to check.</param>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 97c14d463..6b9397437 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Networking.Tests
CallBase = true
};
configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf);
- return (IConfigurationManager)configManager.Object;
+ return configManager.Object;
}
/// <summary>
@@ -35,9 +35,9 @@ namespace Jellyfin.Networking.Tests
// eth16 only
[InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
// All interfaces excluded. (including loopbacks)
- [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[127.0.0.1/8,::1/128]")]
+ [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[]")]
// vEthernet1 and vEthernet212 should be excluded.
- [InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24,127.0.0.1/8,::1/128]")]
+ [InlineData("192.168.1.200/24,-20,vEthernet1|192.168.2.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.200/24", "[200.200.200.200/24]")]
// Overlapping interface,
[InlineData("192.168.1.110/24,-20,br0|192.168.1.10/24,-16,br0|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.110/24,192.168.1.10/24]")]
public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
@@ -476,5 +476,51 @@ namespace Jellyfin.Networking.Tests
Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIp)), denied);
}
+
+ [Theory]
+ [InlineData("192.168.1.209/24,-16,eth16", "192.168.1.0/24", "", "192.168.1.209")] // Only 1 address so use it.
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "", "192.168.1.208")] // LAN address is specified by default.
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "10.0.0.1", "10.0.0.1")] // return bind address
+
+ public void GetBindInterface_NoSourceGiven_Success(string interfaces, string lan, string bind, string result)
+ {
+ var conf = new NetworkConfiguration
+ {
+ EnableIPV4 = true,
+ LocalNetworkSubnets = lan.Split(','),
+ LocalNetworkAddresses = bind.Split(',')
+ };
+
+ NetworkManager.MockNetworkSettings = interfaces;
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ var interfaceToUse = nm.GetBindInterface(string.Empty, out _);
+
+ Assert.Equal(result, interfaceToUse);
+ }
+
+ [Theory]
+ [InlineData("192.168.1.209/24,-16,eth16", "192.168.1.0/24", "", "192.168.1.210", "192.168.1.209")] // Source on LAN
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "", "192.168.1.209", "192.168.1.208")] // Source on LAN
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "", "8.8.8.8", "10.0.0.1")] // Source external.
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "10.0.0.1", "192.168.1.209", "10.0.0.1")] // LAN not bound, so return external.
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "8.8.8.8", "10.0.0.1")] // return external bind address
+ [InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "192.168.1.210", "192.168.1.208")] // return LAN bind address
+ public void GetBindInterface_ValidSourceGiven_Success(string interfaces, string lan, string bind, string source, string result)
+ {
+ var conf = new NetworkConfiguration
+ {
+ EnableIPV4 = true,
+ LocalNetworkSubnets = lan.Split(','),
+ LocalNetworkAddresses = bind.Split(',')
+ };
+
+ NetworkManager.MockNetworkSettings = interfaces;
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+
+ var interfaceToUse = nm.GetBindInterface(source, out _);
+
+ Assert.Equal(result, interfaceToUse);
+ }
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 14bd53db5..10767ae23 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -1,23 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="3.0.3">
+ <PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
new file mode 100644
index 000000000..2ba5c47d7
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -0,0 +1,597 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Manager;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.Manager
+{
+ public class ItemImageProviderTests
+ {
+ private const string TestDataImagePath = "Test Data/Images/blank{0}.jpg";
+
+ [Fact]
+ public void ValidateImages_PhotoEmptyProviders_NoChange()
+ {
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(new Photo(), Enumerable.Empty<ILocalImageProvider>(), null);
+
+ Assert.False(changed);
+ }
+
+ [Fact]
+ public void ValidateImages_EmptyItemEmptyProviders_NoChange()
+ {
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(new Video(), Enumerable.Empty<ILocalImageProvider>(), null);
+
+ Assert.False(changed);
+ }
+
+ private static TheoryData<ImageType, int> GetImageTypesWithCount()
+ {
+ var theoryTypes = new TheoryData<ImageType, int>
+ {
+ // minimal test cases that hit different handling
+ { ImageType.Primary, 1 },
+ { ImageType.Backdrop, 1 },
+ { ImageType.Backdrop, 2 }
+ };
+
+ return theoryTypes;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void ValidateImages_EmptyItemAndPopulatedProviders_AddsImages(ImageType imageType, int imageCount)
+ {
+ // Has to exist for querying DateModified time on file, results stored but not checked so not populating
+ BaseItem.FileSystem = Mock.Of<IFileSystem>();
+
+ var item = new Video();
+ var imageProvider = GetImageProvider(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(item, new[] { imageProvider }, null);
+
+ Assert.True(changed);
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void ValidateImages_PopulatedItemWithGoodPathsAndEmptyProviders_NoChange(ImageType imageType, int imageCount)
+ {
+ var item = GetItemWithImages(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
+
+ Assert.False(changed);
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void ValidateImages_PopulatedItemWithBadPathsAndEmptyProviders_RemovesImage(ImageType imageType, int imageCount)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
+
+ Assert.True(changed);
+ Assert.Empty(item.GetImages(imageType));
+ }
+
+ [Fact]
+ public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
+ {
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>());
+
+ Assert.False(changed);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void MergeImages_PopulatedItemWithGoodPathsAndPopulatedNewImages_AddsUpdatesImages(ImageType imageType, int imageCount)
+ {
+ // valid and not valid paths - should replace the valid paths with the invalid ones
+ var item = GetItemWithImages(imageType, imageCount, true);
+ var images = GetImages(imageType, imageCount, false);
+
+ var itemImageProvider = GetItemImageProvider(null, null);
+ var changed = itemImageProvider.MergeImages(item, images);
+
+ Assert.True(changed);
+ // adds for types that allow multiple, replaces singular type images
+ if (item.AllowsMultipleImages(imageType))
+ {
+ Assert.Equal(imageCount * 2, item.GetImages(imageType).Count());
+ }
+ else
+ {
+ Assert.Single(item.GetImages(imageType));
+ Assert.Same(images[0].FileInfo.FullName, item.GetImages(imageType).First().Path);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImages_NoChange(ImageType imageType, int imageCount)
+ {
+ var oldTime = new DateTime(1970, 1, 1);
+
+ // match update time with time added to item images (unix epoch)
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
+ .Returns(oldTime);
+ BaseItem.FileSystem = fileSystem.Object;
+
+ // all valid paths - matching for strictly updating
+ var item = GetItemWithImages(imageType, imageCount, true);
+ // set size to non-zero to allow for updates to occur
+ foreach (var image in item.GetImages(imageType))
+ {
+ image.DateModified = oldTime;
+ image.Height = 1;
+ image.Width = 1;
+ }
+
+ var images = GetImages(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, fileSystem);
+ var changed = itemImageProvider.MergeImages(item, images);
+
+ Assert.False(changed);
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImagesWithNewTimestamps_ResetsImageSizes(ImageType imageType, int imageCount)
+ {
+ var oldTime = new DateTime(1970, 1, 1);
+ var updatedTime = new DateTime(2021, 1, 1);
+
+ var fileSystem = new Mock<IFileSystem>();
+ fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
+ .Returns(updatedTime);
+ BaseItem.FileSystem = fileSystem.Object;
+
+ // all valid paths - matching for strictly updating
+ var item = GetItemWithImages(imageType, imageCount, true);
+ // set size to non-zero to allow for image size reset to occur
+ foreach (var image in item.GetImages(imageType))
+ {
+ image.DateModified = oldTime;
+ image.Height = 1;
+ image.Width = 1;
+ }
+
+ var images = GetImages(imageType, imageCount, true);
+
+ var itemImageProvider = GetItemImageProvider(null, fileSystem);
+ var changed = itemImageProvider.MergeImages(item, images);
+
+ Assert.True(changed);
+ // before and after paths are the same, verify updated by size reset to 0
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ foreach (var image in item.GetImages(imageType))
+ {
+ Assert.Equal(updatedTime, image.DateModified);
+ Assert.Equal(0, image.Height);
+ Assert.Equal(0, image.Width);
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 1, false)]
+ [InlineData(ImageType.Backdrop, 2, false)]
+ [InlineData(ImageType.Primary, 1, true)]
+ [InlineData(ImageType.Backdrop, 2, true)]
+ public async void RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var imageResponse = new DynamicImageResponse
+ {
+ HasImage = true,
+ Format = ImageFormat.Jpg,
+ Path = "url path",
+ Protocol = MediaProtocol.Http
+ };
+
+ var dynamicProvider = new Mock<IDynamicImageProvider>(MockBehavior.Strict);
+ dynamicProvider.Setup(rp => rp.Name).Returns("MockDynamicProvider");
+ dynamicProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+ dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>()))
+ .ReturnsAsync(imageResponse);
+
+ var refreshOptions = forceRefresh
+ ? new ImageRefreshOptions(Mock.Of<IDirectoryService>())
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllImages = true
+ }
+ : new ImageRefreshOptions(Mock.Of<IDirectoryService>());
+
+ var itemImageProvider = GetItemImageProvider(null, new Mock<IFileSystem>());
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(forceRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ if (forceRefresh)
+ {
+ // replaces multi-types
+ Assert.Single(item.GetImages(imageType));
+ }
+ else
+ {
+ // adds to multi-types if room
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 1, true, MediaProtocol.Http)]
+ [InlineData(ImageType.Backdrop, 2, true, MediaProtocol.Http)]
+ [InlineData(ImageType.Primary, 1, true, MediaProtocol.File)]
+ [InlineData(ImageType.Backdrop, 2, true, MediaProtocol.File)]
+ [InlineData(ImageType.Primary, 1, false, MediaProtocol.File)]
+ [InlineData(ImageType.Backdrop, 2, false, MediaProtocol.File)]
+ public async void RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
+ {
+ // Has to exist for querying DateModified time on file, results stored but not checked so not populating
+ BaseItem.FileSystem = Mock.Of<IFileSystem>();
+
+ var item = new Video();
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ // Path must exist if set: is read in as a stream by AsyncFile.OpenRead
+ var imageResponse = new DynamicImageResponse
+ {
+ HasImage = true,
+ Format = ImageFormat.Jpg,
+ Path = responseHasPath ? string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0) : null,
+ Protocol = protocol
+ };
+
+ var dynamicProvider = new Mock<IDynamicImageProvider>(MockBehavior.Strict);
+ dynamicProvider.Setup(rp => rp.Name).Returns("MockDynamicProvider");
+ dynamicProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+ dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>()))
+ .ReturnsAsync(imageResponse);
+
+ var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>());
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
+ .Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
+ .Returns(Task.CompletedTask);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.True(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ // dynamic provider unable to return multiple images
+ Assert.Single(item.GetImages(imageType));
+ if (protocol == MediaProtocol.Http)
+ {
+ Assert.Equal(imageResponse.Path, item.GetImagePath(imageType, 0));
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 1, false)]
+ [InlineData(ImageType.Backdrop, 1, false)]
+ [InlineData(ImageType.Backdrop, 2, false)]
+ [InlineData(ImageType.Primary, 1, true)]
+ [InlineData(ImageType.Backdrop, 1, true)]
+ [InlineData(ImageType.Backdrop, 2, true)]
+ public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = forceRefresh
+ ? new ImageRefreshOptions(Mock.Of<IDirectoryService>())
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllImages = true
+ }
+ : new ImageRefreshOptions(Mock.Of<IDirectoryService>());
+
+ var remoteInfo = new RemoteImageInfo[imageCount];
+ for (int i = 0; i < imageCount; i++)
+ {
+ remoteInfo[i] = new RemoteImageInfo
+ {
+ Type = imageType,
+ Url = "image url " + i,
+ Width = 1 // min width is set to 0, this will always pass
+ };
+ }
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, new Mock<IFileSystem>());
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(forceRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ foreach (var image in item.GetImages(imageType))
+ {
+ if (forceRefresh)
+ {
+ Assert.Matches(@"image url [0-9]", image.Path);
+ }
+ else
+ {
+ Assert.DoesNotMatch(@"image url [0-9]", image.Path);
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData(ImageType.Primary, 0, false)] // singular type only fetches if type is missing from item, no caching
+ [InlineData(ImageType.Backdrop, 0, false)] // empty item, no cache to check
+ [InlineData(ImageType.Backdrop, 1, false)] // populated item, cached so no download
+ [InlineData(ImageType.Backdrop, 1, true)] // populated item, forced to download
+ public async void RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
+ {
+ var targetImageCount = 1;
+
+ // Set path and media source manager so images will be downloaded (EnableImageStub will return false)
+ var item = GetItemWithImages(imageType, initialImageCount, false);
+ item.Path = "non-empty path";
+ BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
+
+ // seek 2 so it won't short-circuit out of downloading when populated
+ var libraryOptions = GetLibraryOptions(item, imageType, 2);
+
+ const string Content = "Content";
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+ remoteProvider.Setup(rp => rp.GetImageResponse(It.IsAny<string>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync((string url, CancellationToken _) => new HttpResponseMessage
+ {
+ ReasonPhrase = url,
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(Content, Encoding.UTF8, "image/jpeg")
+ });
+
+ var refreshOptions = fullRefresh
+ ? new ImageRefreshOptions(Mock.Of<IDirectoryService>())
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllImages = true
+ }
+ : new ImageRefreshOptions(Mock.Of<IDirectoryService>());
+
+ var remoteInfo = new RemoteImageInfo[targetImageCount];
+ for (int i = 0; i < targetImageCount; i++)
+ {
+ remoteInfo[i] = new RemoteImageInfo()
+ {
+ Type = imageType,
+ Url = "image url " + i,
+ Width = 1 // min width is set to 0, this will always pass
+ };
+ }
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
+ .Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) =>
+ callbackItem.SetImagePath(callbackType, callbackItem.AllowsMultipleImages(callbackType) ? callbackItem.GetImages(callbackType).Count() : 0, new FileSystemMetadata()))
+ .Returns(Task.CompletedTask);
+ var fileSystem = new Mock<IFileSystem>();
+ // match reported file size to image content length - condition for skipping already downloaded multi-images
+ fileSystem.Setup(fs => fs.GetFileInfo(It.IsAny<string>()))
+ .Returns(new FileSystemMetadata { Length = Content.Length });
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, fileSystem);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(initialImageCount == 0 || fullRefresh, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ Assert.Equal(targetImageCount, item.GetImages(imageType).Count());
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
+ {
+ var item = new Video();
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>());
+
+ // populate remote with double the required images to verify count is trimmed to the library option count
+ var remoteInfoCount = imageCount * 2;
+ var remoteInfo = new RemoteImageInfo[remoteInfoCount];
+ for (int i = 0; i < remoteInfoCount; i++)
+ {
+ remoteInfo[i] = new RemoteImageInfo()
+ {
+ Type = imageType,
+ Url = "image url " + i,
+ Width = 1 // min width is set to 0, this will always pass
+ };
+ }
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.True(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ var actualImages = item.GetImages(imageType).ToList();
+ Assert.Equal(imageCount, actualImages.Count);
+ // images from the provider manager are sorted by preference (earlier images are higher priority) so we can verify that low url numbers are chosen
+ foreach (var image in actualImages)
+ {
+ var index = int.Parse(Regex.Match(image.Path, @"[0-9]+").Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ Assert.True(index < imageCount);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImageTypesWithCount))]
+ public async void RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
+ {
+ var item = GetItemWithImages(imageType, imageCount, false);
+
+ var libraryOptions = GetLibraryOptions(item, imageType, imageCount);
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>())
+ {
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllImages = true
+ };
+
+ var itemImageProvider = GetItemImageProvider(Mock.Of<IProviderManager>(), null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.False(result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ }
+
+ private static ItemImageProvider GetItemImageProvider(IProviderManager? providerManager, Mock<IFileSystem>? mockFileSystem)
+ {
+ // strict to ensure this isn't accidentally used where a prepared mock is intended
+ providerManager ??= Mock.Of<IProviderManager>(MockBehavior.Strict);
+
+ // BaseItem.ValidateImages depends on the directory service being able to list directory contents, give it the expected valid file paths
+ mockFileSystem ??= new Mock<IFileSystem>(MockBehavior.Strict);
+ mockFileSystem.Setup(fs => fs.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()))
+ .Returns(new[]
+ {
+ string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 0),
+ string.Format(CultureInfo.InvariantCulture, TestDataImagePath, 1)
+ });
+
+ return new ItemImageProvider(new NullLogger<ItemImageProvider>(), providerManager, mockFileSystem.Object);
+ }
+
+ private static BaseItem GetItemWithImages(ImageType type, int count, bool validPaths)
+ {
+ // Has to exist for querying DateModified time on file, results stored but not checked so not populating
+ BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
+
+ var item = new Video();
+
+ var path = validPaths ? TestDataImagePath : "invalid path {0}";
+ for (int i = 0; i < count; i++)
+ {
+ item.SetImagePath(type, i, new FileSystemMetadata
+ {
+ FullName = string.Format(CultureInfo.InvariantCulture, path, i),
+ });
+ }
+
+ return item;
+ }
+
+ private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths)
+ {
+ var images = GetImages(type, count, validPaths);
+
+ var imageProvider = new Mock<ILocalImageProvider>();
+ imageProvider.Setup(ip => ip.GetImages(It.IsAny<BaseItem>(), It.IsAny<IDirectoryService>()))
+ .Returns(images);
+ return imageProvider.Object;
+ }
+
+ /// <summary>
+ /// Creates a list of <see cref="LocalImageInfo"/> references of the specified type and size, optionally pointing to files that exist.
+ /// </summary>
+ private static LocalImageInfo[] GetImages(ImageType type, int count, bool validPaths)
+ {
+ var path = validPaths ? TestDataImagePath : "invalid path {0}";
+ var images = new LocalImageInfo[count];
+ for (int i = 0; i < count; i++)
+ {
+ images[i] = new LocalImageInfo
+ {
+ Type = type,
+ FileInfo = new FileSystemMetadata
+ {
+ FullName = string.Format(CultureInfo.InvariantCulture, path, i)
+ }
+ };
+ }
+
+ return images;
+ }
+
+ /// <summary>
+ /// Generates a <see cref="LibraryOptions"/> object that will allow for the requested number of images for the target type.
+ /// </summary>
+ private static LibraryOptions GetLibraryOptions(BaseItem item, ImageType type, int count)
+ {
+ return new LibraryOptions
+ {
+ TypeOptions = new[]
+ {
+ new TypeOptions
+ {
+ Type = item.GetType().Name,
+ ImageOptions = new[]
+ {
+ new ImageOption
+ {
+ Type = type,
+ Limit = count,
+ MinWidth = 0
+ }
+ }
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
new file mode 100644
index 000000000..558321810
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo
+{
+ public class EmbeddedImageProviderTests
+ {
+ [Theory]
+ [InlineData(typeof(AudioBook))]
+ [InlineData(typeof(BoxSet))]
+ [InlineData(typeof(Series))]
+ [InlineData(typeof(Season))]
+ [InlineData(typeof(Episode), ImageType.Primary)]
+ [InlineData(typeof(Movie), ImageType.Logo, ImageType.Backdrop, ImageType.Primary)]
+ public void GetSupportedImages_AnyBaseItem_ReturnsExpected(Type type, params ImageType[] expected)
+ {
+ BaseItem item = (BaseItem)Activator.CreateInstance(type)!;
+ var embeddedImageProvider = new EmbeddedImageProvider(Mock.Of<IMediaSourceManager>(), Mock.Of<IMediaEncoder>(), new NullLogger<EmbeddedImageProvider>());
+ var actual = embeddedImageProvider.GetSupportedImages(item);
+ Assert.Equal(expected.OrderBy(i => i.ToString()), actual.OrderBy(i => i.ToString()));
+ }
+
+ [Fact]
+ public async void GetImage_NoStreams_ReturnsNoImage()
+ {
+ var input = new Movie();
+
+ var mediaSourceManager = GetMediaSourceManager(input, new List<MediaAttachment>(), new List<MediaStream>());
+ var embeddedImageProvider = new EmbeddedImageProvider(mediaSourceManager, null, new NullLogger<EmbeddedImageProvider>());
+
+ var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Theory]
+ [InlineData("chapter", null, 1, ImageType.Chapter, null)] // unexpected type, nothing found
+ [InlineData("unmatched", null, 1, ImageType.Primary, null)] // doesn't default on no match
+ [InlineData("clearlogo.png", null, 1, ImageType.Logo, ImageFormat.Png)] // extract extension from name
+ [InlineData("backdrop", "image/bmp", 2, ImageType.Backdrop, ImageFormat.Bmp)] // extract extension from mimetype
+ [InlineData("poster", null, 3, ImageType.Primary, ImageFormat.Jpg)] // default extension to jpg
+ public async void GetImage_Attachment_ReturnsCorrectSelection(string filename, string mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat)
+ {
+ var attachments = new List<MediaAttachment>();
+ string pathPrefix = "path";
+ for (int i = 1; i <= targetIndex; i++)
+ {
+ var name = i == targetIndex ? filename : "unmatched";
+ attachments.Add(new ()
+ {
+ FileName = name,
+ MimeType = mimetype,
+ Index = i
+ });
+ }
+
+ var input = new Movie();
+
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<int>(), It.IsAny<ImageFormat>(), It.IsAny<CancellationToken>()))
+ .Returns<string, string, MediaSourceInfo, MediaStream, int, ImageFormat, CancellationToken>((_, _, _, _, index, ext, _) => Task.FromResult(pathPrefix + index + "." + ext));
+ var mediaSourceManager = GetMediaSourceManager(input, attachments, new List<MediaStream>());
+ var embeddedImageProvider = new EmbeddedImageProvider(mediaSourceManager, mediaEncoder.Object, new NullLogger<EmbeddedImageProvider>());
+
+ var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None);
+ Assert.NotNull(actual);
+ if (expectedFormat == null)
+ {
+ Assert.False(actual.HasImage);
+ }
+ else
+ {
+ Assert.True(actual.HasImage);
+ Assert.Equal(pathPrefix + targetIndex + "." + expectedFormat, actual.Path, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal(expectedFormat, actual.Format);
+ }
+ }
+
+ [Theory]
+ [InlineData("chapter", null, 1, ImageType.Chapter, null)] // unexpected type, nothing found
+ [InlineData(null, null, 1, ImageType.Backdrop, null)] // no label, can only find primary
+ [InlineData(null, null, 1, ImageType.Primary, ImageFormat.Jpg)] // no label, finds primary
+ [InlineData("backdrop", null, 2, ImageType.Backdrop, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream
+ [InlineData("cover", null, 2, ImageType.Primary, ImageFormat.Jpg)] // uses label to find index 2, not just pulling first stream
+ [InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)]
+ [InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)]
+ [InlineData(null, "gif", 1, ImageType.Primary, ImageFormat.Gif)]
+ public async void GetImage_Embedded_ReturnsCorrectSelection(string label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat)
+ {
+ var streams = new List<MediaStream>();
+ for (int i = 1; i <= targetIndex; i++)
+ {
+ var comment = i == targetIndex ? label : "unmatched";
+ streams.Add(new ()
+ {
+ Type = MediaStreamType.EmbeddedImage,
+ Index = i,
+ Comment = comment,
+ Codec = codec
+ });
+ }
+
+ var input = new Movie();
+
+ var pathPrefix = "path";
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<int>(), It.IsAny<ImageFormat>(), It.IsAny<CancellationToken>()))
+ .Returns<string, string, MediaSourceInfo, MediaStream, int, ImageFormat, CancellationToken>((_, _, _, stream, index, ext, _) =>
+ {
+ Assert.Equal(streams[index - 1], stream);
+ return Task.FromResult(pathPrefix + index + "." + ext);
+ });
+ var mediaSourceManager = GetMediaSourceManager(input, new List<MediaAttachment>(), streams);
+ var embeddedImageProvider = new EmbeddedImageProvider(mediaSourceManager, mediaEncoder.Object, new NullLogger<EmbeddedImageProvider>());
+
+ var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None);
+ Assert.NotNull(actual);
+ if (expectedFormat == null)
+ {
+ Assert.False(actual.HasImage);
+ }
+ else
+ {
+ Assert.True(actual.HasImage);
+ Assert.Equal(pathPrefix + targetIndex + "." + expectedFormat, actual.Path, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal(expectedFormat, actual.Format);
+ }
+ }
+
+ private static IMediaSourceManager GetMediaSourceManager(BaseItem item, List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams)
+ {
+ var mediaSourceManager = new Mock<IMediaSourceManager>(MockBehavior.Strict);
+ mediaSourceManager.Setup(i => i.GetMediaAttachments(item.Id))
+ .Returns(mediaAttachments);
+ mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId == item.Id && q.Type == MediaStreamType.EmbeddedImage)))
+ .Returns(mediaStreams);
+ return mediaSourceManager.Object;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
index b160e676e..33da277e3 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CA1002 // Do not expose generic lists
+#pragma warning disable CA1002 // Do not expose generic lists
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
@@ -11,11 +11,12 @@ namespace Jellyfin.Providers.Tests.MediaInfo
{
public class SubtitleResolverTests
{
- public static IEnumerable<object[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
+ public static TheoryData<List<MediaStream>, string, int, string[], MediaStream[]> AddExternalSubtitleStreams_GivenMixedFilenames_ReturnsValidSubtitles_TestData()
{
+ var data = new TheoryData<List<MediaStream>, string, int, string[], MediaStream[]>();
+
var index = 0;
- yield return new object[]
- {
+ data.Add(
new List<MediaStream>(),
"/video/My.Video.mkv",
index,
@@ -52,8 +53,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo
CreateMediaStream("/video/My.Video.default.forced.en.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.en.default.forced.srt", "srt", "en", index++, isForced: true, isDefault: true),
CreateMediaStream("/video/My.Video.With.Additional.Garbage.en.srt", "srt", "en", index),
- }
- };
+ });
+
+ return data;
}
[Theory]
@@ -78,6 +80,37 @@ namespace Jellyfin.Providers.Tests.MediaInfo
}
}
+ [Theory]
+ [InlineData("/video/My Video.mkv", "/video/My Video.srt", "srt", null, false, false)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.srt", "srt", null, false, false)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.foreign.srt", "srt", null, true, false)]
+ [InlineData("/video/My Video.mkv", "/video/My Video.forced.srt", "srt", null, true, false)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.default.srt", "srt", null, false, true)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.forced.default.srt", "srt", null, true, true)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.en.srt", "srt", "en", false, false)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.default.en.srt", "srt", "en", false, true)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.default.forced.en.srt", "srt", "en", true, true)]
+ [InlineData("/video/My.Video.mkv", "/video/My.Video.en.default.forced.srt", "srt", "en", true, true)]
+ public void AddExternalSubtitleStreams_GivenSingleFile_ReturnsExpectedSubtitle(string videoPath, string file, string codec, string? language, bool isForced, bool isDefault)
+ {
+ var streams = new List<MediaStream>();
+ var expected = CreateMediaStream(file, codec, language, 0, isForced, isDefault);
+
+ new SubtitleResolver(Mock.Of<ILocalizationManager>()).AddExternalSubtitleStreams(streams, videoPath, 0, new[] { file });
+
+ Assert.Single(streams);
+
+ var actual = streams[0];
+
+ Assert.Equal(expected.Index, actual.Index);
+ Assert.Equal(expected.Type, actual.Type);
+ Assert.Equal(expected.IsExternal, actual.IsExternal);
+ Assert.Equal(expected.Path, actual.Path);
+ Assert.Equal(expected.IsDefault, actual.IsDefault);
+ Assert.Equal(expected.IsForced, actual.IsForced);
+ Assert.Equal(expected.Language, actual.Language);
+ }
+
private static MediaStream CreateMediaStream(string path, string codec, string? language, int index, bool isForced = false, bool isDefault = false)
{
return new ()
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs
new file mode 100644
index 000000000..839925dd1
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.MediaInfo
+{
+ public class VideoImageProviderTests
+ {
+ private static TheoryData<Video> GetImage_UnsupportedInput_ReturnsNoImage_TestData()
+ {
+ return new ()
+ {
+ new Movie { IsPlaceHolder = true },
+
+ new Movie { DefaultVideoStreamIndex = null },
+
+ // set a default index but don't put anything there (invalid input, but provider shouldn't break)
+ new Movie { DefaultVideoStreamIndex = 0 }
+ };
+ }
+
+ [Theory]
+ [MemberData(nameof(GetImage_UnsupportedInput_ReturnsNoImage_TestData))]
+ public async void GetImage_UnsupportedInput_ReturnsNoImage(Video input)
+ {
+ var mediaSourceManager = GetMediaSourceManager(input, null, new List<MediaStream>());
+ var videoImageProvider = new VideoImageProvider(mediaSourceManager, Mock.Of<IMediaEncoder>(), new NullLogger<VideoImageProvider>());
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.False(actual.HasImage);
+ }
+
+ [Theory]
+ [InlineData(1, 1)] // default not first stream
+ [InlineData(5, 0)] // default out of valid range
+ public async void GetImage_DefaultVideoStreams_ReturnsCorrectStreamImage(int defaultIndex, int targetIndex)
+ {
+ var input = new Movie { DefaultVideoStreamIndex = defaultIndex };
+
+ string targetPath = "path.jpg";
+ var mediaStreams = new List<MediaStream>();
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+
+ for (int i = 0; i <= targetIndex; i++)
+ {
+ var mediaStream = new MediaStream { Type = MediaStreamType.Video, Index = i };
+ mediaStreams.Add(mediaStream);
+
+ var path = i == targetIndex ? targetPath : "wrong stream called!";
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), mediaStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), It.IsAny<CancellationToken>()))
+ .Returns(Task.FromResult(path));
+ }
+
+ var defaultStream = defaultIndex < mediaStreams.Count ? mediaStreams[targetIndex] : null;
+ var mediaSourceManager = GetMediaSourceManager(input, defaultStream, mediaStreams);
+
+ var videoImageProvider = new VideoImageProvider(mediaSourceManager, mediaEncoder.Object, new NullLogger<VideoImageProvider>());
+
+ var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ Assert.NotNull(actual);
+ Assert.True(actual.HasImage);
+ Assert.Equal(targetPath, actual.Path);
+ Assert.Equal(ImageFormat.Jpg, actual.Format);
+ }
+
+ [Theory]
+ [InlineData(null, 10)] // default time
+ [InlineData(500, 50)] // calculated time
+ public async void GetImage_TimeSpan_SelectsCorrectTime(int? runTimeSeconds, long expectedSeconds)
+ {
+ MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
+ var input = new Movie
+ {
+ DefaultVideoStreamIndex = 0,
+ RunTimeTicks = runTimeSeconds * TimeSpan.TicksPerSecond
+ };
+
+ var mediaSourceManager = GetMediaSourceManager(input, targetStream, new List<MediaStream> { targetStream });
+
+ // use a callback to catch the actual value
+ // provides more information on failure than verifying a specific input was called on the mock
+ TimeSpan? actualTimeSpan = null;
+ var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
+ mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
+ .Callback<string, string, MediaSourceInfo, MediaStream, Video3DFormat?, TimeSpan?, CancellationToken>((_, _, _, _, _, timeSpan, _) => actualTimeSpan = timeSpan)
+ .Returns(Task.FromResult("path"));
+
+ var videoImageProvider = new VideoImageProvider(mediaSourceManager, mediaEncoder.Object, new NullLogger<VideoImageProvider>());
+
+ // not testing return, just verifying what gets requested for time span
+ await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+
+ Assert.Equal(TimeSpan.FromSeconds(expectedSeconds), actualTimeSpan);
+ }
+
+ private static IMediaSourceManager GetMediaSourceManager(Video item, MediaStream? defaultStream, List<MediaStream> mediaStreams)
+ {
+ var defaultStreamList = new List<MediaStream>();
+ if (defaultStream != null)
+ {
+ defaultStreamList.Add(defaultStream);
+ }
+
+ var mediaSourceManager = new Mock<IMediaSourceManager>(MockBehavior.Strict);
+ mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId == item.Id && q.Index == item.DefaultVideoStreamIndex)))
+ .Returns(defaultStreamList);
+ mediaSourceManager.Setup(i => i.GetMediaStreams(It.Is<MediaStreamQuery>(q => q.ItemId == item.Id && q.Type == MediaStreamType.Video)))
+ .Returns(mediaStreams);
+ return mediaSourceManager.Object;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs
index efe8063a0..25900bc09 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonOmdbConverterTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs
@@ -1,10 +1,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Providers.Plugins.Omdb;
using Xunit;
-namespace Jellyfin.Common.Tests.Json
+namespace Jellyfin.Providers.Tests.Omdb
{
public class JsonOmdbConverterTests
{
diff --git a/tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank0.jpg
diff --git a/tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Test Data/Images/blank1.jpg
diff --git a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs
index f6a7c676f..efd2d9553 100644
--- a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs
@@ -23,5 +23,18 @@ namespace Jellyfin.Providers.Tests.Tmdb
{
Assert.Equal(expected, TmdbUtils.NormalizeLanguage(input!));
}
+
+ [Theory]
+ [InlineData(null, null, null)]
+ [InlineData(null, "en-US", null)]
+ [InlineData("en", null, "en")]
+ [InlineData("en", "en-US", "en-US")]
+ [InlineData("fr-CA", "fr-BE", "fr-CA")]
+ [InlineData("fr-CA", "fr", "fr-CA")]
+ [InlineData("de", "en-US", "de")]
+ public static void AdjustImageLanguage_Valid_Success(string imageLanguage, string requestLanguage, string expected)
+ {
+ Assert.Equal(expected, TmdbUtils.AdjustImageLanguage(imageLanguage, requestLanguage));
+ }
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index f312933fb..6337dea41 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -32,10 +32,11 @@ namespace Jellyfin.Server.Implementations.Tests.Data
_sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
}
- public static IEnumerable<object[]> ItemImageInfoFromValueString_Valid_TestData()
+ public static TheoryData<string, ItemImageInfo> ItemImageInfoFromValueString_Valid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, ItemImageInfo>();
+
+ data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo
{
@@ -45,41 +46,33 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Width = 1920,
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*0*0",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0*Primary*600",
new ItemImageInfo
{
Path = "https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg",
Type = ImageType.Primary,
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"%MetadataPath%/library/68/68578562b96c80a7ebd530848801f645/poster.jpg*637264380567586027*Primary*600*336",
new ItemImageInfo
{
@@ -88,8 +81,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
DateModified = new DateTime(637264380567586027, DateTimeKind.Utc),
Width = 600,
Height = 336
- }
- };
+ });
+
+ return data;
}
[Theory]
@@ -109,15 +103,18 @@ namespace Jellyfin.Server.Implementations.Tests.Data
[InlineData("")]
[InlineData("*")]
[InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
+ [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*6374520964785129080*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid modified date
+ [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*-637452096478512963*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Negative modified date
+ [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Invalid*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid type
public void ItemImageInfoFromValueString_Invalid_Null(string value)
{
Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
}
- public static IEnumerable<object[]> DeserializeImages_Valid_TestData()
+ public static TheoryData<string, ItemImageInfo[]> DeserializeImages_Valid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, ItemImageInfo[]>();
+ data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN",
new ItemImageInfo[]
{
@@ -130,11 +127,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/poster.jpg*637261226720645297*Primary*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/logo.png*637261226720805297*Logo*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/landscape.jpg*637261226721285297*Thumb*0*0|%MetadataPath%/library/2a/2a27372f1e9bc757b1db99721bbeae1e/backdrop.jpg*637261226721685297*Backdrop*0*0",
new ItemImageInfo[]
{
@@ -162,20 +157,19 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Type = ImageType.Backdrop,
DateModified = new DateTime(637261226721685297, DateTimeKind.Utc),
}
- }
- };
+ });
+
+ return data;
}
- public static IEnumerable<object[]> DeserializeImages_ValidAndInvalid_TestData()
+ public static TheoryData<string, ItemImageInfo[]> DeserializeImages_ValidAndInvalid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, ItemImageInfo[]>();
+ data.Add(
string.Empty,
- Array.Empty<ItemImageInfo>()
- };
+ Array.Empty<ItemImageInfo>());
- yield return new object[]
- {
+ data.Add(
"/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Primary*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN|test|1234||ss",
new ItemImageInfo[]
{
@@ -188,14 +182,13 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Height = 1080,
BlurHash = "WjQbtJtSO8nhNZ%L_Io#R*oaS6o}-;adXAoIn7j[%hW9s:WGw[nN"
}
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"|",
- Array.Empty<ItemImageInfo>()
- };
+ Array.Empty<ItemImageInfo>());
+
+ return data;
}
[Theory]
@@ -239,30 +232,27 @@ namespace Jellyfin.Server.Implementations.Tests.Data
Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
}
- public static IEnumerable<object[]> DeserializeProviderIds_Valid_TestData()
+ public static TheoryData<string, Dictionary<string, string>> DeserializeProviderIds_Valid_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, Dictionary<string, string>>();
+
+ data.Add(
"Imdb=tt0119567",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"Imdb=tt0119567|Tmdb=330|TmdbCollection=328",
new Dictionary<string, string>()
{
{ "Imdb", "tt0119567" },
{ "Tmdb", "330" },
{ "TmdbCollection", "328" },
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970",
new Dictionary<string, string>()
{
@@ -271,8 +261,9 @@ namespace Jellyfin.Server.Implementations.Tests.Data
{ "AudioDbArtist", "111352" },
{ "AudioDbAlbum", "2116560" },
{ "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" },
- }
- };
+ });
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
index 30e6542f9..d991f5574 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs
@@ -1,10 +1,10 @@
+using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.IO;
-using MediaBrowser.Model.System;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.IO
@@ -31,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.IO
{
var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
- if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ if (OperatingSystem.IsWindows())
{
var expectedWindowsPath = expectedAbsolutePath.Replace('/', '\\');
Assert.Equal(expectedWindowsPath, generatedPath.Split(':')[1]);
@@ -55,7 +55,7 @@ namespace Jellyfin.Server.Implementations.Tests.IO
[SkippableFact]
public void GetFileInfo_DanglingSymlink_ExistsFalse()
{
- Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
+ Skip.If(OperatingSystem.IsWindows());
string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link");
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index adbca8344..028ebdf55 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -6,11 +6,8 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
<RootNamespace>Jellyfin.Server.Implementations.Tests</RootNamespace>
</PropertyGroup>
@@ -24,12 +21,12 @@
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
index c393742eb..362c3216f 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs
@@ -1,4 +1,4 @@
-using System;
+using Emby.Naming.Common;
using Emby.Server.Implementations.Library.Resolvers.TV;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
@@ -14,22 +14,21 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
public class EpisodeResolverTest
{
+ private static readonly NamingOptions _namingOptions = new ();
+
[Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{
- var season = new Season { Name = "Season 1" };
var parent = new Folder { Name = "extras" };
- var libraryManagerMock = new Mock<ILibraryManager>();
- libraryManagerMock.Setup(x => x.GetItemById(It.IsAny<Guid>())).Returns(season);
- var episodeResolver = new EpisodeResolver(libraryManagerMock.Object);
+ var episodeResolver = new EpisodeResolver(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = parent,
CollectionType = CollectionType.TvShows,
- FileInfo = new FileSystemMetadata()
+ FileInfo = new FileSystemMetadata
{
FullName = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv"
}
@@ -45,14 +44,14 @@ namespace Jellyfin.Server.Implementations.Tests.Library
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
- var episodeResolver = new EpisodeResolverMock(Mock.Of<ILibraryManager>());
+ var episodeResolver = new EpisodeResolverMock(_namingOptions);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = series,
CollectionType = CollectionType.TvShows,
- FileInfo = new FileSystemMetadata()
+ FileInfo = new FileSystemMetadata
{
FullName = "Extras/Extras S01E01.mkv"
}
@@ -62,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
private class EpisodeResolverMock : EpisodeResolver
{
- public EpisodeResolverMock(ILibraryManager libraryManager) : base(libraryManager)
+ public EpisodeResolverMock(NamingOptions namingOptions) : base(namingOptions)
{
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
new file mode 100644
index 000000000..b29426d85
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -0,0 +1,232 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library.Resolvers;
+using Emby.Server.Implementations.Library.Resolvers.Audio;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library.LibraryManager;
+
+public class FindExtrasTests
+{
+ private readonly Emby.Server.Implementations.Library.LibraryManager _libraryManager;
+ private readonly Mock<IFileSystem> _fileSystemMock;
+
+ public FindExtrasTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ fixture.Register(() => new NamingOptions());
+ var configMock = fixture.Freeze<Mock<IServerConfigurationManager>>();
+ configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data");
+ _fileSystemMock = fixture.Freeze<Mock<IFileSystem>>();
+ _fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny<string>())).Returns<string>(path => new FileSystemMetadata { FullName = path });
+ _libraryManager = fixture.Build<Emby.Server.Implementations.Library.LibraryManager>().Do(s => s.AddParts(
+ fixture.Create<IEnumerable<IResolverIgnoreRule>>(),
+ new List<IItemResolver> { new GenericVideoResolver<Video>(fixture.Create<NamingOptions>()), new AudioResolver(fixture.Create<NamingOptions>()) },
+ fixture.Create<IEnumerable<IIntroProvider>>(),
+ fixture.Create<IEnumerable<IBaseItemComparer>>(),
+ fixture.Create<IEnumerable<ILibraryPostScanTask>>()))
+ .Create();
+
+ // This is pretty terrible but unavoidable
+ BaseItem.FileSystem ??= fixture.Create<IFileSystem>();
+ BaseItem.MediaSourceManager ??= fixture.Create<IMediaSourceManager>();
+ }
+
+ [Fact]
+ public void FindExtras_SeparateMovieFolder_FindsCorrectExtras()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up.mkv",
+ "/movies/Up/Up - trailer.mkv",
+ "/movies/Up/Up - sample.mkv",
+ "/movies/Up/Up something else.mkv"
+ };
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ IsDirectory = false
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
+
+ Assert.Equal(2, extras.Count);
+ Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
+ Assert.Equal(ExtraType.Sample, extras[1].ExtraType);
+ }
+
+ [Fact]
+ public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsCorrectExtras()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up.mkv",
+ "/movies/Up/Up - trailer.mkv",
+ "/movies/Up/trailers",
+ "/movies/Up/theme-music",
+ "/movies/Up/theme.mp3",
+ "/movies/Up/not a theme.mp3",
+ "/movies/Up/behind the scenes",
+ "/movies/Up/behind the scenes.mkv",
+ "/movies/Up/Up - sample.mkv",
+ "/movies/Up/Up something else.mkv"
+ };
+
+ _fileSystemMock.Setup(f => f.GetFiles(
+ "/movies/Up/trailers",
+ It.IsAny<string[]>(),
+ false,
+ false))
+ .Returns(new List<FileSystemMetadata>
+ {
+ new ()
+ {
+ FullName = "/movies/Up/trailers/some trailer.mkv",
+ Name = "some trailer.mkv",
+ IsDirectory = false
+ }
+ });
+
+ _fileSystemMock.Setup(f => f.GetFiles(
+ "/movies/Up/behind the scenes",
+ It.IsAny<string[]>(),
+ false,
+ false))
+ .Returns(new List<FileSystemMetadata>
+ {
+ new ()
+ {
+ FullName = "/movies/Up/behind the scenes/the making of Up.mkv",
+ Name = "the making of Up.mkv",
+ IsDirectory = false
+ }
+ });
+
+ _fileSystemMock.Setup(f => f.GetFiles(
+ "/movies/Up/theme-music",
+ It.IsAny<string[]>(),
+ false,
+ false))
+ .Returns(new List<FileSystemMetadata>
+ {
+ new ()
+ {
+ FullName = "/movies/Up/theme-music/theme2.mp3",
+ Name = "theme2.mp3",
+ IsDirectory = false
+ }
+ });
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ Name = Path.GetFileName(p),
+ IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p))
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
+
+ Assert.Equal(6, extras.Count);
+ Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
+ Assert.Equal(ExtraType.Trailer, extras[1].ExtraType);
+ Assert.Equal(ExtraType.BehindTheScenes, extras[2].ExtraType);
+ Assert.Equal(ExtraType.Sample, extras[3].ExtraType);
+ Assert.Equal(ExtraType.ThemeSong, extras[4].ExtraType);
+ Assert.Equal(ExtraType.ThemeSong, extras[5].ExtraType);
+ }
+
+ [Fact]
+ public void FindExtras_SeparateMovieFolderWithMixedExtras_FindsOnlyExtrasInMovieFolder()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up.mkv",
+ "/movies/Up/trailer.mkv",
+ "/movies/Another Movie/trailer.mkv"
+ };
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ IsDirectory = false
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
+
+ Assert.Single(extras);
+ Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
+ Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
+ Assert.Equal("/movies/Up/trailer.mkv", extras[0].Path);
+ }
+
+ [Fact]
+ public void FindExtras_SeparateMovieFolderWithParts_FindsCorrectExtras()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up - part1.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up - part1.mkv",
+ "/movies/Up/Up - part2.mkv",
+ "/movies/Up/trailer.mkv",
+ "/movies/Another Movie/trailer.mkv"
+ };
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ IsDirectory = false
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
+
+ Assert.Single(extras);
+ Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
+ Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
+ Assert.Equal("/movies/Up/trailer.mkv", extras[0].Path);
+ }
+
+ [Fact]
+ public void FindExtras_SeriesWithTrailers_FindsCorrectExtras()
+ {
+ var owner = new Series { Name = "Dexter", Path = "/series/Dexter" };
+ var paths = new List<string>
+ {
+ "/series/Dexter/Season 1/S01E01.mkv",
+ "/series/Dexter/trailer.mkv",
+ "/series/Dexter/trailers/trailer2.mkv",
+ };
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ IsDirectory = string.IsNullOrEmpty(Path.GetExtension(p))
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
+
+ Assert.Equal(2, extras.Count);
+ Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
+ Assert.Equal("trailer", extras[0].FileNameWithoutExtension);
+ Assert.Equal("/series/Dexter/trailer.mkv", extras[0].Path);
+ Assert.Equal("/series/Dexter/trailers/trailer2.mkv", extras[1].Path);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs
new file mode 100644
index 000000000..8ed3d8b94
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs
@@ -0,0 +1,32 @@
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.IO;
+using Emby.Server.Implementations.Library;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library
+{
+ public class MediaSourceManagerTests
+ {
+ private readonly MediaSourceManager _mediaSourceManager;
+
+ public MediaSourceManagerTests()
+ {
+ IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ fixture.Inject<IFileSystem>(fixture.Create<ManagedFileSystem>());
+ _mediaSourceManager = fixture.Create<MediaSourceManager>();
+ }
+
+ [Theory]
+ [InlineData(@"C:\mydir\myfile.ext", MediaProtocol.File)]
+ [InlineData("/mydir/myfile.ext", MediaProtocol.File)]
+ [InlineData("file:///mydir/myfile.ext", MediaProtocol.File)]
+ [InlineData("http://example.com/stream.m3u8", MediaProtocol.Http)]
+ [InlineData("https://example.com/stream.m3u8", MediaProtocol.Http)]
+ [InlineData("rtsp://media.example.com:554/twister/audiotrack", MediaProtocol.Rtsp)]
+ public void GetPathProtocol_ValidArg_Correct(string path, MediaProtocol expected)
+ => Assert.Equal(expected, _mediaSourceManager.GetPathProtocol(path));
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index c5cc056f5..54a63a5f2 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -11,6 +11,18 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son", "something", null)]
+ [InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "tmdbid", "618355")]
+ [InlineData("[tmdbid=618355]", "tmdbid", "618355")]
+ [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
+ [InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
+ [InlineData("tmdbid=618355]", "tmdbid", null)]
+ [InlineData("[tmdbid=618355", "tmdbid", null)]
+ [InlineData("tmdbid=618355", "tmdbid", null)]
+ [InlineData("tmdbid=", "tmdbid", null)]
+ [InlineData("tmdbid", "tmdbid", null)]
+ [InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
index e8b93b437..09aec82b0 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using Emby.Server.Implementations.LiveTv.EmbyTV;
using MediaBrowser.Controller.LiveTv;
using Xunit;
@@ -8,43 +7,36 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
public static class RecordingHelperTests
{
- public static IEnumerable<object[]> GetRecordingName_Success_TestData()
+ public static TheoryData<string, TimerInfo> GetRecordingName_Success_TestData()
{
- yield return new object[]
- {
+ var data = new TheoryData<string, TimerInfo>();
+
+ data.Add(
"The Incredibles 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Incredibles",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsMovie = true
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"The Incredibles (2004)",
new TimerInfo
{
Name = "The Incredibles",
IsMovie = true,
ProductionYear = 2004
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory 2020_04_20_21_06_00",
new TimerInfo
{
Name = "The Big Bang Theory",
StartDate = new DateTime(2020, 4, 20, 21, 6, 0, DateTimeKind.Local),
IsProgramSeries = true,
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory S12E10",
new TimerInfo
{
@@ -52,11 +44,8 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
SeasonNumber = 12,
EpisodeNumber = 10
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory S12E10 The VCR Illumination",
new TimerInfo
{
@@ -65,34 +54,27 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
SeasonNumber = 12,
EpisodeNumber = 10,
EpisodeTitle = "The VCR Illumination"
- }
- };
-
- yield return new object[]
- {
+ });
+ data.Add(
"The Big Bang Theory 2018-12-06",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
- OriginalAirDate = new DateTime(2018, 12, 6)
- }
- };
+ OriginalAirDate = new DateTime(2018, 12, 6, 0, 0, 0, DateTimeKind.Local)
+ });
- yield return new object[]
- {
+ data.Add(
"The Big Bang Theory 2018-12-06 - The VCR Illumination",
new TimerInfo
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
- OriginalAirDate = new DateTime(2018, 12, 6),
+ OriginalAirDate = new DateTime(2018, 12, 6, 0, 0, 0, DateTimeKind.Local),
EpisodeTitle = "The VCR Illumination"
- }
- };
+ });
- yield return new object[]
- {
+ data.Add(
"The Big Bang Theory 2018_12_06_21_06_00 - The VCR Illumination",
new TimerInfo
{
@@ -101,8 +83,9 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
IsProgramSeries = true,
OriginalAirDate = new DateTime(2018, 12, 6),
EpisodeTitle = "The VCR Illumination"
- }
- };
+ });
+
+ return data;
}
[Theory]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
new file mode 100644
index 000000000..3b3e38bd1
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
@@ -0,0 +1,240 @@
+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 Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
+{
+ public class SchedulesDirectDeserializeTests
+ {
+ private readonly JsonSerializerOptions _jsonOptions;
+
+ public SchedulesDirectDeserializeTests()
+ {
+ _jsonOptions = JsonDefaults.Options;
+ }
+
+ /// <summary>
+ /// /token reponse.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Token_Response_Live_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/token_live_response.json");
+ var tokenDto = JsonSerializer.Deserialize<TokenDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(tokenDto);
+ Assert.Equal(0, tokenDto!.Code);
+ Assert.Equal("OK", tokenDto.Message);
+ Assert.Equal("AWS-SD-web.1", tokenDto.ServerId);
+ Assert.Equal(new DateTime(2016, 08, 23, 13, 55, 25, DateTimeKind.Utc), tokenDto.TokenTimestamp);
+ Assert.Equal("f3fca79989cafe7dead71beefedc812b", tokenDto.Token);
+ }
+
+ /// <summary>
+ /// /token response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Token_Response_Offline_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/token_offline_response.json");
+ var tokenDto = JsonSerializer.Deserialize<TokenDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(tokenDto);
+ Assert.Equal(3_000, tokenDto!.Code);
+ Assert.Equal("Server offline for maintenance.", tokenDto.Message);
+ Assert.Equal("20141201.web.1", tokenDto.ServerId);
+ Assert.Equal(new DateTime(2015, 04, 23, 00, 03, 32, DateTimeKind.Utc), tokenDto.TokenTimestamp);
+ Assert.Equal("CAFEDEADBEEFCAFEDEADBEEFCAFEDEADBEEFCAFE", tokenDto.Token);
+ Assert.Equal("SERVICE_OFFLINE", tokenDto.Response);
+ }
+
+ /// <summary>
+ /// /schedules request.
+ /// </summary>
+ [Fact]
+ public void Serialize_Schedule_Request_Success()
+ {
+ var expectedString = File.ReadAllText("Test Data/SchedulesDirect/schedules_request.json").Trim();
+
+ var requestObject = new RequestScheduleForChannelDto[]
+ {
+ new RequestScheduleForChannelDto
+ {
+ StationId = "20454",
+ Date = new[]
+ {
+ "2015-03-13",
+ "2015-03-17"
+ }
+ },
+ new RequestScheduleForChannelDto
+ {
+ StationId = "10021",
+ Date = new[]
+ {
+ "2015-03-12",
+ "2015-03-13"
+ }
+ }
+ };
+
+ var requestString = JsonSerializer.Serialize(requestObject, _jsonOptions);
+ Assert.Equal(expectedString, requestString);
+ }
+
+ /// <summary>
+ /// /schedules response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Schedule_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/schedules_response.json");
+ var days = JsonSerializer.Deserialize<IReadOnlyList<DayDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(days);
+ Assert.Equal(1, days!.Count);
+
+ var dayDto = days[0];
+ Assert.Equal("20454", dayDto.StationId);
+ Assert.Equal(2, dayDto.Programs.Count);
+
+ Assert.Equal("SH005371070000", dayDto.Programs[0].ProgramId);
+ Assert.Equal(new DateTime(2015, 03, 03, 00, 00, 00, DateTimeKind.Utc), dayDto.Programs[0].AirDateTime);
+ Assert.Equal(1_800, dayDto.Programs[0].Duration);
+ Assert.Equal("Sy8HEMBPcuiAx3FBukUhKQ", dayDto.Programs[0].Md5);
+ Assert.True(dayDto.Programs[0].New);
+ Assert.Equal(2, dayDto.Programs[0].AudioProperties.Count);
+ Assert.Equal("stereo", dayDto.Programs[0].AudioProperties[0]);
+ Assert.Equal("cc", dayDto.Programs[0].AudioProperties[1]);
+ Assert.Equal(1, dayDto.Programs[0].VideoProperties.Count);
+ Assert.Equal("hdtv", dayDto.Programs[0].VideoProperties[0]);
+ }
+
+ /// <summary>
+ /// /programs response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Program_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/programs_response.json");
+ var programDtos = JsonSerializer.Deserialize<IReadOnlyList<ProgramDetailsDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(programDtos);
+ Assert.Equal(2, programDtos!.Count);
+ Assert.Equal("EP000000060003", programDtos[0].ProgramId);
+ Assert.Equal(1, programDtos[0].Titles.Count);
+ Assert.Equal("'Allo 'Allo!", programDtos[0].Titles[0].Title120);
+ Assert.Equal("Series", programDtos[0].EventDetails?.SubType);
+ Assert.Equal("en", programDtos[0].Descriptions?.Description1000[0].DescriptionLanguage);
+ Assert.Equal("A disguised British Intelligence officer is sent to help the airmen.", programDtos[0].Descriptions?.Description1000[0].Description);
+ Assert.Equal(new DateTime(1985, 11, 04), programDtos[0].OriginalAirDate);
+ Assert.Equal(1, programDtos[0].Genres.Count);
+ Assert.Equal("Sitcom", programDtos[0].Genres[0]);
+ Assert.Equal("The Poloceman Cometh", programDtos[0].EpisodeTitle150);
+ Assert.Equal(2, programDtos[0].Metadata[0].Gracenote?.Season);
+ Assert.Equal(3, programDtos[0].Metadata[0].Gracenote?.Episode);
+ Assert.Equal(13, programDtos[0].Cast.Count);
+ Assert.Equal("383774", programDtos[0].Cast[0].PersonId);
+ Assert.Equal("392649", programDtos[0].Cast[0].NameId);
+ Assert.Equal("Gorden Kaye", programDtos[0].Cast[0].Name);
+ Assert.Equal("Actor", programDtos[0].Cast[0].Role);
+ Assert.Equal("01", programDtos[0].Cast[0].BillingOrder);
+ Assert.Equal(3, programDtos[0].Crew.Count);
+ Assert.Equal("354407", programDtos[0].Crew[0].PersonId);
+ Assert.Equal("363281", programDtos[0].Crew[0].NameId);
+ Assert.Equal("David Croft", programDtos[0].Crew[0].Name);
+ Assert.Equal("Director", programDtos[0].Crew[0].Role);
+ Assert.Equal("01", programDtos[0].Crew[0].BillingOrder);
+ }
+
+ /// <summary>
+ /// /metadata/programs response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Metadata_Programs_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/metadata_programs_response.json");
+ var showImagesDtos = JsonSerializer.Deserialize<IReadOnlyList<ShowImagesDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(showImagesDtos);
+ Assert.Equal(1, showImagesDtos!.Count);
+ Assert.Equal("SH00712240", showImagesDtos[0].ProgramId);
+ Assert.Equal(4, showImagesDtos[0].Data.Count);
+ Assert.Equal("135", showImagesDtos[0].Data[0].Width);
+ Assert.Equal("180", showImagesDtos[0].Data[0].Height);
+ Assert.Equal("assets/p282288_b_v2_aa.jpg", showImagesDtos[0].Data[0].Uri);
+ Assert.Equal("Sm", showImagesDtos[0].Data[0].Size);
+ Assert.Equal("3x4", showImagesDtos[0].Data[0].Aspect);
+ Assert.Equal("Banner-L3", showImagesDtos[0].Data[0].Category);
+ Assert.Equal("yes", showImagesDtos[0].Data[0].Text);
+ Assert.Equal("true", showImagesDtos[0].Data[0].Primary);
+ Assert.Equal("Series", showImagesDtos[0].Data[0].Tier);
+ }
+
+ /// <summary>
+ /// /headends response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Headends_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/headends_response.json");
+ var headendsDtos = JsonSerializer.Deserialize<IReadOnlyList<HeadendsDto>>(bytes, _jsonOptions);
+
+ Assert.NotNull(headendsDtos);
+ Assert.Equal(8, headendsDtos!.Count);
+ Assert.Equal("CA00053", headendsDtos[0].Headend);
+ Assert.Equal("Cable", headendsDtos[0].Transport);
+ Assert.Equal("Beverly Hills", headendsDtos[0].Location);
+ Assert.Equal(2, headendsDtos[0].Lineups.Count);
+ Assert.Equal("Time Warner Cable - Cable", headendsDtos[0].Lineups[0].Name);
+ Assert.Equal("USA-CA00053-DEFAULT", headendsDtos[0].Lineups[0].Lineup);
+ Assert.Equal("/20141201/lineups/USA-CA00053-DEFAULT", headendsDtos[0].Lineups[0].Uri);
+ }
+
+ /// <summary>
+ /// /lineups response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Lineups_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/lineups_response.json");
+ var lineupsDto = JsonSerializer.Deserialize<LineupsDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(lineupsDto);
+ Assert.Equal(0, lineupsDto!.Code);
+ Assert.Equal("20141201.web.1", lineupsDto.ServerId);
+ Assert.Equal(new DateTime(2015, 04, 17, 14, 22, 17, DateTimeKind.Utc), lineupsDto.LineupTimestamp);
+ Assert.Equal(5, lineupsDto.Lineups.Count);
+ Assert.Equal("GBR-0001317-DEFAULT", lineupsDto.Lineups[0].Lineup);
+ Assert.Equal("Freeview - Carlton - LWT (Southeast)", lineupsDto.Lineups[0].Name);
+ Assert.Equal("DVB-T", lineupsDto.Lineups[0].Transport);
+ Assert.Equal("London", lineupsDto.Lineups[0].Location);
+ Assert.Equal("/20141201/lineups/GBR-0001317-DEFAULT", lineupsDto.Lineups[0].Uri);
+
+ Assert.Equal("DELETED LINEUP", lineupsDto.Lineups[4].Name);
+ Assert.True(lineupsDto.Lineups[4].IsDeleted);
+ }
+
+ /// <summary>
+ /// /lineup/:id response.
+ /// </summary>
+ [Fact]
+ public void Deserialize_Lineup_Response_Success()
+ {
+ var bytes = File.ReadAllBytes("Test Data/SchedulesDirect/lineup_response.json");
+ var channelDto = JsonSerializer.Deserialize<ChannelDto>(bytes, _jsonOptions);
+
+ Assert.NotNull(channelDto);
+ Assert.Equal(2, channelDto!.Map.Count);
+ Assert.Equal("24326", channelDto.Map[0].StationId);
+ Assert.Equal("001", channelDto.Map[0].Channel);
+ Assert.Equal("BBC ONE South", channelDto.Map[0].ProvderCallsign);
+ Assert.Equal("1", channelDto.Map[0].LogicalChannelNumber);
+ Assert.Equal("providerCallsign", channelDto.Map[0].MatchType);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
new file mode 100644
index 000000000..3e7d6ed1d
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Localization;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Localization
+{
+ public class LocalizationManagerTests
+ {
+ [Fact]
+ public void GetCountries_All_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ var countries = localizationManager.GetCountries().ToList();
+
+ Assert.Equal(139, countries.Count);
+
+ var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal));
+ Assert.NotNull(germany);
+ Assert.Equal("Germany", germany!.DisplayName);
+ Assert.Equal("DEU", germany.ThreeLetterISORegionName);
+ Assert.Equal("DE", germany.TwoLetterISORegionName);
+ }
+
+ [Fact]
+ public async Task GetCultures_All_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+ var cultures = localizationManager.GetCultures().ToList();
+
+ Assert.Equal(190, cultures.Count);
+
+ var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
+ Assert.NotNull(germany);
+ Assert.Equal("ger", germany!.ThreeLetterISOLanguageName);
+ Assert.Equal("German", germany.DisplayName);
+ Assert.Equal("German", germany.Name);
+ Assert.Contains("deu", germany.ThreeLetterISOLanguageNames);
+ Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
+ }
+
+ [Theory]
+ [InlineData("de")]
+ [InlineData("ger")]
+ [InlineData("german")]
+ public async Task FindLanguageInfo_Valid_Success(string identifier)
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+
+ var germany = localizationManager.FindLanguageInfo(identifier);
+ Assert.NotNull(germany);
+
+ Assert.Equal("ger", germany!.ThreeLetterISOLanguageName);
+ Assert.Equal("German", germany.DisplayName);
+ Assert.Equal("German", germany.Name);
+ Assert.Contains("deu", germany.ThreeLetterISOLanguageNames);
+ Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
+ }
+
+ [Fact]
+ public async Task GetParentalRatings_Default_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+ var ratings = localizationManager.GetParentalRatings().ToList();
+
+ Assert.Equal(23, ratings.Count);
+
+ var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
+ Assert.NotNull(tvma);
+ Assert.Equal(9, tvma!.Value);
+ }
+
+ [Fact]
+ public async Task GetParentalRatings_ConfiguredCountryCode_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ MetadataCountryCode = "DE"
+ });
+ await localizationManager.LoadAll();
+ var ratings = localizationManager.GetParentalRatings().ToList();
+
+ Assert.Equal(10, ratings.Count);
+
+ var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
+ Assert.NotNull(fsk);
+ Assert.Equal(7, fsk!.Value);
+ }
+
+ [Theory]
+ [InlineData("CA-R", "CA", 10)]
+ [InlineData("FSK-16", "DE", 8)]
+ [InlineData("FSK-18", "DE", 9)]
+ [InlineData("FSK-18", "US", 9)]
+ [InlineData("TV-MA", "US", 9)]
+ [InlineData("XXX", "asdf", 100)]
+ [InlineData("Germany: FSK-18", "DE", 9)]
+ public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ MetadataCountryCode = countryCode
+ });
+ await localizationManager.LoadAll();
+ var level = localizationManager.GetRatingLevel(value);
+ Assert.NotNull(level);
+ Assert.Equal(expectedLevel, level!);
+ }
+
+ [Fact]
+ public async Task GetRatingLevel_GivenUnratedString_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ UICulture = "de-DE"
+ });
+ await localizationManager.LoadAll();
+ Assert.Null(localizationManager.GetRatingLevel("n/a"));
+ }
+
+ [Theory]
+ [InlineData("Default", "Default")]
+ [InlineData("HeaderLiveTV", "Live TV")]
+ public void GetLocalizedString_Valid_Success(string key, string expected)
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ UICulture = "en-US"
+ });
+
+ var translated = localizationManager.GetLocalizedString(key);
+ Assert.NotNull(translated);
+ Assert.Equal(expected, translated);
+ }
+
+ [Fact]
+ public void GetLocalizedString_Invalid_Success()
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ UICulture = "en-US"
+ });
+
+ var key = "SuperInvalidTranslationKeyThatWillNeverBeAdded";
+
+ var translated = localizationManager.GetLocalizedString(key);
+ Assert.NotNull(translated);
+ Assert.Equal(key, translated);
+ }
+
+ private LocalizationManager Setup(ServerConfiguration config)
+ {
+ var mockConfiguration = new Mock<IServerConfigurationManager>();
+ mockConfiguration.SetupGet(x => x.Configuration).Returns(config);
+
+ return new LocalizationManager(mockConfiguration.Object, new NullLogger<LocalizationManager>());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs
new file mode 100644
index 000000000..28d832ef8
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.QuickConnect;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Configuration;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.QuickConnect
+{
+ public class QuickConnectManagerTests
+ {
+ private static readonly AuthorizationInfo _quickConnectAuthInfo = new AuthorizationInfo
+ {
+ Device = "Device",
+ DeviceId = "DeviceId",
+ Client = "Client",
+ Version = "1.0.0"
+ };
+
+ private readonly Fixture _fixture;
+ private readonly ServerConfiguration _config;
+ private readonly QuickConnectManager _quickConnectManager;
+
+ public QuickConnectManagerTests()
+ {
+ _config = new ServerConfiguration();
+ var configManager = new Mock<IServerConfigurationManager>();
+ configManager.Setup(x => x.Configuration).Returns(_config);
+
+ _fixture = new Fixture();
+ _fixture.Customize(new AutoMoqCustomization
+ {
+ ConfigureMembers = true
+ }).Inject(configManager.Object);
+
+ // User object contains circular references.
+ _fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList()
+ .ForEach(b => _fixture.Behaviors.Remove(b));
+ _fixture.Behaviors.Add(new OmitOnRecursionBehavior());
+
+ _quickConnectManager = _fixture.Create<QuickConnectManager>();
+ }
+
+ [Fact]
+ public void IsEnabled_QuickConnectUnavailable_False()
+ => Assert.False(_quickConnectManager.IsEnabled);
+
+ [Theory]
+ [InlineData("", "DeviceId", "Client", "1.0.0")]
+ [InlineData("Device", "", "Client", "1.0.0")]
+ [InlineData("Device", "DeviceId", "", "1.0.0")]
+ [InlineData("Device", "DeviceId", "Client", "")]
+ public void TryConnect_InvalidAuthorizationInfo_ThrowsArgumentException(string device, string deviceId, string client, string version)
+ => Assert.Throws<ArgumentException>(() => _quickConnectManager.TryConnect(
+ new AuthorizationInfo
+ {
+ Device = device,
+ DeviceId = deviceId,
+ Client = client,
+ Version = version
+ }));
+
+ [Fact]
+ public void TryConnect_QuickConnectUnavailable_ThrowsAuthenticationException()
+ => Assert.Throws<AuthenticationException>(() => _quickConnectManager.TryConnect(_quickConnectAuthInfo));
+
+ [Fact]
+ public void CheckRequestStatus_QuickConnectUnavailable_ThrowsAuthenticationException()
+ => Assert.Throws<AuthenticationException>(() => _quickConnectManager.CheckRequestStatus(string.Empty));
+
+ [Fact]
+ public void AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
+ => Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty));
+
+ [Fact]
+ public void GetAuthorizedRequest_QuickConnectUnavailable_ThrowsAuthenticationException()
+ => Assert.Throws<AuthenticationException>(() => _quickConnectManager.GetAuthorizedRequest(string.Empty));
+
+ [Fact]
+ public void IsEnabled_QuickConnectAvailable_True()
+ {
+ _config.QuickConnectAvailable = true;
+ Assert.True(_quickConnectManager.IsEnabled);
+ }
+
+ [Fact]
+ public void CheckRequestStatus_QuickConnectAvailable_Success()
+ {
+ _config.QuickConnectAvailable = true;
+ var res1 = _quickConnectManager.TryConnect(_quickConnectAuthInfo);
+ var res2 = _quickConnectManager.CheckRequestStatus(res1.Secret);
+ Assert.Equal(res1, res2);
+ }
+
+ [Fact]
+ public void CheckRequestStatus_UnknownSecret_ThrowsResourceNotFoundException()
+ {
+ _config.QuickConnectAvailable = true;
+ Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.CheckRequestStatus("Unknown secret"));
+ }
+
+ [Fact]
+ public void GetAuthorizedRequest_UnknownSecret_ThrowsResourceNotFoundException()
+ {
+ _config.QuickConnectAvailable = true;
+ Assert.Throws<ResourceNotFoundException>(() => _quickConnectManager.GetAuthorizedRequest("Unknown secret"));
+ }
+
+ [Fact]
+ public async Task AuthorizeRequest_QuickConnectAvailable_Success()
+ {
+ _config.QuickConnectAvailable = true;
+ var res = _quickConnectManager.TryConnect(_quickConnectAuthInfo);
+ Assert.True(await _quickConnectManager.AuthorizeRequest(Guid.Empty, res.Code));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
new file mode 100644
index 000000000..59d82678e
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Sorting/AiredEpisodeOrderComparerTests.cs
@@ -0,0 +1,164 @@
+using System;
+using Emby.Server.Implementations.Sorting;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Sorting
+{
+ public class AiredEpisodeOrderComparerTests
+ {
+ [Theory]
+ [ClassData(typeof(EpisodeBadData))]
+ public void Compare_GivenNull_ThrowsArgumentNullException(BaseItem? x, BaseItem? y)
+ {
+ var cmp = new AiredEpisodeOrderComparer();
+ Assert.Throws<ArgumentNullException>(() => cmp.Compare(x, y));
+ }
+
+ [Theory]
+ [ClassData(typeof(EpisodeTestData))]
+ public void AiredEpisodeOrderCompareTest(BaseItem x, BaseItem y, int expected)
+ {
+ var cmp = new AiredEpisodeOrderComparer();
+
+ Assert.Equal(expected, cmp.Compare(x, y));
+ Assert.Equal(-expected, cmp.Compare(y, x));
+ }
+
+ private class EpisodeBadData : TheoryData<BaseItem?, BaseItem?>
+ {
+ public EpisodeBadData()
+ {
+ Add(null, new Episode());
+ Add(new Episode(), null);
+ }
+ }
+
+ private class EpisodeTestData : TheoryData<BaseItem, BaseItem, int>
+ {
+ public EpisodeTestData()
+ {
+ Add(
+ new Movie(),
+ new Movie(),
+ 0);
+
+ Add(
+ new Movie(),
+ new Episode(),
+ 1);
+
+ // Good cases
+ Add(
+ new Episode(),
+ new Episode(),
+ 0);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ 0);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 2, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ 1);
+
+ // Good Specials
+ Add(
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
+ 0);
+
+ Add(
+ new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
+ 1);
+
+ // Specials to Episodes
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 2 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 3, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsAfterSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 2 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
+ 1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
+ 0);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 3 },
+ new Episode { ParentIndexNumber = 0, IndexNumber = 1, AirsBeforeSeasonNumber = 1, AirsBeforeEpisodeNumber = 2 },
+ 1);
+
+ // Premiere Date
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ 0);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ -1);
+
+ Add(
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 12, 0, 0, 0) },
+ new Episode { ParentIndexNumber = 1, IndexNumber = 1, PremiereDate = new DateTime(2021, 09, 11, 0, 0, 0) },
+ 1);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json
new file mode 100644
index 000000000..015afeecc
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json
@@ -0,0 +1 @@
+[{"headend":"CA00053","transport":"Cable","location":"Beverly Hills","lineups":[{"name":"Time Warner Cable - Cable","lineup":"USA-CA00053-DEFAULT","uri":"/20141201/lineups/USA-CA00053-DEFAULT"},{"name":"Time Warner Cable - Digital","lineup":"USA-CA00053-X","uri":"/20141201/lineups/USA-CA00053-X"}]},{"headend":"CA61222","transport":"Cable","location":"Beverly Hills","lineups":[{"name":"Mulholland Estates - Cable","lineup":"USA-CA61222-DEFAULT","uri":"/20141201/lineups/USA-CA61222-DEFAULT"}]},{"headend":"CA66511","transport":"Cable","location":"Los Angeles","lineups":[{"name":"AT&T U-verse TV - Digital","lineup":"USA-CA66511-X","uri":"/20141201/lineups/USA-CA66511-X"}]},{"headend":"CA67309","transport":"Cable","location":"Westchester","lineups":[{"name":"Time Warner Cable Sherman Oaks - Cable","lineup":"USA-CA67309-DEFAULT","uri":"/20141201/lineups/USA-CA67309-DEFAULT"},{"name":"Time Warner Cable Sherman Oaks - Digital","lineup":"USA-CA67309-X","uri":"/20141201/lineups/USA-CA67309-X"}]},{"headend":"CA67310","transport":"Cable","location":"Eagle Rock","lineups":[{"name":"Time Warner Cable City of Los Angeles - Cable","lineup":"USA-CA67310-DEFAULT","uri":"/20141201/lineups/USA-CA67310-DEFAULT"},{"name":"Time Warner Cable City of Los Angeles - Digital","lineup":"USA-CA67310-X","uri":"/20141201/lineups/USA-CA67310-X"}]},{"headend":"DISH803","transport":"Satellite","location":"Los Angeles","lineups":[{"name":"DISH Los Angeles - Satellite","lineup":"USA-DISH803-DEFAULT","uri":"/20141201/lineups/USA-DISH803-DEFAULT"}]},{"headend":"DITV803","transport":"Satellite","location":"Los Angeles","lineups":[{"name":"DIRECTV Los Angeles - Satellite","lineup":"USA-DITV803-DEFAULT","uri":"/20141201/lineups/USA-DITV803-DEFAULT"}]},{"headend":"90210","transport":"Antenna","location":"90210","lineups":[{"name":"Antenna","lineup":"USA-OTA-90210","uri":"/20141201/lineups/USA-OTA-90210"}]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json
new file mode 100644
index 000000000..072089470
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json
@@ -0,0 +1 @@
+{"map":[{"stationID":"24326","channel":"001","providerCallsign":"BBC ONE South","logicalChannelNumber":"1","matchType":"providerCallsign"},{"stationID":"17154","channel":"002","providerCallsign":"BBC TWO","logicalChannelNumber":"2","matchType":"providerCallsign"}]}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json
new file mode 100644
index 000000000..032a84e59
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json
@@ -0,0 +1 @@
+{"code":0,"serverID":"20141201.web.1","datetime":"2015-04-17T14:22:17Z","lineups":[{"lineup":"GBR-0001317-DEFAULT","name":"Freeview - Carlton - LWT (Southeast)","transport":"DVB-T","location":"London","uri":"/20141201/lineups/GBR-0001317-DEFAULT"},{"lineup":"USA-IL57303-X","name":"Comcast Waukegan/Lake Forest Area - Digital","transport":"Cable","location":"Lake Forest","uri":"/20141201/lineups/USA-IL57303-X"},{"lineup":"USA-NY67791-X","name":"Verizon Fios Queens - Digital","transport":"Cable","location":"Fresh Meadows","uri":"/20141201/lineups/USA-NY67791-X"},{"lineup":"USA-OTA-60030","name":"Local Over the Air Broadcast","transport":"Antenna","location":"60030","uri":"/20141201/lineups/USA-OTA-60030"},{"lineup":"USA-WI61859-DEFAULT","name":"DELETED LINEUP","isDeleted":true}]}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
new file mode 100644
index 000000000..78166f09a
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json
@@ -0,0 +1 @@
+[{"programID":"SH00712240","data":[{"width":"135","height":"180","uri":"assets/p282288_b_v2_aa.jpg","size":"Sm","aspect":"3x4","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"720","height":"540","uri":"assets/p282288_b_h6_aa.jpg","size":"Lg","aspect":"4x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"960","height":"1440","uri":"assets/p282288_b_v8_aa.jpg","size":"Ms","aspect":"2x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"},{"width":"180","height":"135","uri":"assets/p282288_b_h5_aa.jpg","size":"Sm","aspect":"4x3","category":"Banner-L3","text":"yes","primary":"true","tier":"Series"}]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json
new file mode 100644
index 000000000..fe2a94436
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json
@@ -0,0 +1 @@
+[{"programID":"EP000000060003","titles":[{"title120":"'Allo 'Allo!"}],"eventDetails":{"subType":"Series"},"descriptions":{"description1000":[{"descriptionLanguage":"en","description":"A disguised British Intelligence officer is sent to help the airmen."}]},"originalAirDate":"1985-11-04","genres":["Sitcom"],"episodeTitle150":"The Poloceman Cometh","metadata":[{"Gracenote":{"season":2,"episode":3}}],"cast":[{"personId":"383774","nameId":"392649","name":"Gorden Kaye","role":"Actor","billingOrder":"01"},{"personId":"246840","nameId":"250387","name":"Carmen Silvera","role":"Actor","billingOrder":"02"},{"personId":"376955","nameId":"385830","name":"Rose Hill","role":"Actor","billingOrder":"03"},{"personId":"259773","nameId":"263340","name":"Vicki Michelle","role":"Actor","billingOrder":"04"},{"personId":"353113","nameId":"361987","name":"Kirsten Cooke","role":"Actor","billingOrder":"05"},{"personId":"77787","nameId":"77787","name":"Richard Marner","role":"Actor","billingOrder":"06"},{"personId":"230921","nameId":"234193","name":"Guy Siner","role":"Actor","billingOrder":"07"},{"personId":"374934","nameId":"383809","name":"Kim Hartman","role":"Actor","billingOrder":"08"},{"personId":"369151","nameId":"378026","name":"Richard Gibson","role":"Actor","billingOrder":"09"},{"personId":"343690","nameId":"352564","name":"Arthur Bostrom","role":"Actor","billingOrder":"10"},{"personId":"352557","nameId":"361431","name":"John D. Collins","role":"Actor","billingOrder":"11"},{"personId":"605275","nameId":"627734","name":"Nicholas Frankau","role":"Actor","billingOrder":"12"},{"personId":"373394","nameId":"382269","name":"Jack Haig","role":"Actor","billingOrder":"13"}],"crew":[{"personId":"354407","nameId":"363281","name":"David Croft","role":"Director","billingOrder":"01"},{"personId":"354407","nameId":"363281","name":"David Croft","role":"Writer","billingOrder":"02"},{"personId":"105145","nameId":"105145","name":"Jeremy Lloyd","role":"Writer","billingOrder":"03"}],"showType":"Series","hasImageArtwork":true,"md5":"Jo5NKxoo44xRvBCAq8QT2A"},{"programID":"EP000000510142","titles":[{"title120":"A Different World"}],"eventDetails":{"subType":"Series"},"descriptions":{"description1000":[{"descriptionLanguage":"en","description":"Whitley and Dwayne tell new students about their honeymoon in Los Angeles."}]},"originalAirDate":"1992-09-24","genres":["Sitcom"],"episodeTitle150":"Honeymoon in L.A.","metadata":[{"Gracenote":{"season":6,"episode":1}}],"cast":[{"personId":"700","nameId":"700","name":"Jasmine Guy","role":"Actor","billingOrder":"01"},{"personId":"729","nameId":"729","name":"Kadeem Hardison","role":"Actor","billingOrder":"02"},{"personId":"120","nameId":"120","name":"Darryl M. Bell","role":"Actor","billingOrder":"03"},{"personId":"1729","nameId":"1729","name":"Cree Summer","role":"Actor","billingOrder":"04"},{"personId":"217","nameId":"217","name":"Charnele Brown","role":"Actor","billingOrder":"05"},{"personId":"1811","nameId":"1811","name":"Glynn Turman","role":"Actor","billingOrder":"06"},{"personId":"1232","nameId":"1232","name":"Lou Myers","role":"Actor","billingOrder":"07"},{"personId":"1363","nameId":"1363","name":"Jada Pinkett","role":"Guest Star","billingOrder":"08"},{"personId":"222967","nameId":"225536","name":"Ajai Sanders","role":"Guest Star","billingOrder":"09"},{"personId":"181744","nameId":"183292","name":"Karen Malina White","role":"Guest Star","billingOrder":"10"},{"personId":"305017","nameId":"318897","name":"Patrick Y. Malone","role":"Guest Star","billingOrder":"11"},{"personId":"9841","nameId":"9841","name":"Bumper Robinson","role":"Guest Star","billingOrder":"12"},{"personId":"426422","nameId":"435297","name":"Sister Souljah","role":"Guest Star","billingOrder":"13"},{"personId":"25","nameId":"25","name":"Debbie Allen","role":"Guest Star","billingOrder":"14"},{"personId":"668","nameId":"668","name":"Gilbert Gottfried","role":"Guest Star","billingOrder":"15"}],"showType":"Series","hasImageArtwork":true,"md5":"P5kz0QmCeYxIA+yL0H4DWw"}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json
new file mode 100644
index 000000000..5ef1bfb1c
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json
@@ -0,0 +1 @@
+[{"stationID":"20454","date":["2015-03-13","2015-03-17"]},{"stationID":"10021","date":["2015-03-12","2015-03-13"]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json
new file mode 100644
index 000000000..4a97e5517
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json
@@ -0,0 +1 @@
+[{"stationID":"20454","programs":[{"programID":"SH005371070000","airDateTime":"2015-03-03T00:00:00Z","duration":1800,"md5":"Sy8HEMBPcuiAx3FBukUhKQ","new":true,"audioProperties":["stereo","cc"],"videoProperties":["hdtv"]},{"programID":"EP000014577244","airDateTime":"2015-03-03T00:30:00Z","duration":1800,"md5":"25DNXVXO192JI7Y9vSW9lQ","new":true,"audioProperties":["stereo","cc"],"videoProperties":["hdtv"]}]}]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json
new file mode 100644
index 000000000..e5fb64a6f
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json
@@ -0,0 +1 @@
+{"code":0,"message":"OK","serverID":"AWS-SD-web.1","datetime":"2016-08-23T13:55:25Z","token":"f3fca79989cafe7dead71beefedc812b"}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json
new file mode 100644
index 000000000..b66a4ed0c
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json
@@ -0,0 +1 @@
+{"response":"SERVICE_OFFLINE","code":3000,"serverID":"20141201.web.1","message":"Server offline for maintenance.","datetime":"2015-04-23T00:03:32Z","token":"CAFEDEADBEEFCAFEDEADBEEFCAFEDEADBEEFCAFE"}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zip b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zip
new file mode 100644
index 000000000..15628e26b
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/empty.zip
Binary files differ
diff --git a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs
new file mode 100644
index 000000000..31f33c682
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Enums;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.TypedBaseItem
+{
+ public class BaseItemKindTests
+ {
+ public static TheoryData<Type> BaseItemKind_TestData()
+ {
+ var data = new TheoryData<Type>();
+
+ var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
+ foreach (var assembly in loadedAssemblies)
+ {
+ if (IsProjectAssemblyName(assembly.FullName))
+ {
+ var baseItemTypes = assembly.GetTypes()
+ .Where(targetType => targetType.IsClass
+ && !targetType.IsAbstract
+ && targetType.IsSubclassOf(typeof(MediaBrowser.Controller.Entities.BaseItem)));
+ foreach (var baseItemType in baseItemTypes)
+ {
+ data.Add(baseItemType);
+ }
+ }
+ }
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(BaseItemKind_TestData))]
+ public void EnumParse_GivenValidBaseItemType_ReturnsEnumValue(Type baseItemDescendantType)
+ {
+ var enumValue = Enum.Parse<BaseItemKind>(baseItemDescendantType.Name);
+ Assert.True(Enum.IsDefined(typeof(BaseItemKind), enumValue));
+ }
+
+ [Theory]
+ [MemberData(nameof(BaseItemKind_TestData))]
+ public void GetBaseItemKind_WhenCalledAfterDefaultCtor_DoesNotThrow(Type baseItemDescendantType)
+ {
+ var defaultConstructor = baseItemDescendantType.GetConstructor(Type.EmptyTypes);
+ var instance = (MediaBrowser.Controller.Entities.BaseItem)defaultConstructor!.Invoke(null);
+ var exception = Record.Exception(() => instance.GetBaseItemKind());
+ Assert.Null(exception);
+ }
+
+ private static bool IsProjectAssemblyName(string? name)
+ {
+ if (name == null)
+ {
+ return false;
+ }
+
+ return name.StartsWith("Jellyfin", StringComparison.OrdinalIgnoreCase)
+ || name.StartsWith("Emby", StringComparison.OrdinalIgnoreCase)
+ || name.StartsWith("MediaBrowser", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
index 70acbfc40..7abd2e685 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -40,7 +40,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
_fixture.Customize(new AutoMoqCustomization
{
ConfigureMembers = true
- }).Inject(http);
+ });
+ _fixture.Inject(http);
_installationManager = _fixture.Create<InstallationManager>();
}
@@ -78,5 +79,32 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
Assert.Single(packages);
}
+
+ [Fact]
+ public async Task InstallPackage_InvalidChecksum_ThrowsInvalidDataException()
+ {
+ var packageInfo = new InstallationInfo()
+ {
+ Name = "Test",
+ SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
+ Checksum = "InvalidChecksum"
+ };
+
+ await Assert.ThrowsAsync<InvalidDataException>(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task InstallPackage_Valid_Success()
+ {
+ var packageInfo = new InstallationInfo()
+ {
+ Name = "Test",
+ SourceUrl = "https://repo.jellyfin.org/releases/plugin/empty/empty.zip",
+ Checksum = "11b5b2f1a9ebc4f66d6ef19018543361"
+ };
+
+ var ex = await Record.ExceptionAsync(() => _installationManager.InstallPackage(packageInfo, CancellationToken.None)).ConfigureAwait(false);
+ Assert.Null(ex);
+ }
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index ea6838682..21131eb97 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -1,13 +1,13 @@
using System;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Net.Http.Headers;
-using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
using Jellyfin.Api.Models.UserDtos;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Xunit;
namespace Jellyfin.Server.Integration.Tests
@@ -26,14 +26,13 @@ namespace Jellyfin.Server.Integration.Tests
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
- using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(
+ using var content = JsonContent.Create(
new AuthenticateUserByName()
{
Username = user!.Name,
Pw = user.Password,
},
- jsonOptions));
- content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ options: jsonOptions);
content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content).ConfigureAwait(false);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
index f5411dcb8..3396a94e5 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
@@ -5,7 +5,7 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Xunit;
namespace Jellyfin.Server.Integration.Tests.Controllers
@@ -40,7 +40,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType);
StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!);
- Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd());
+ Assert.Equal(await response.Content.ReadAsStringAsync().ConfigureAwait(false), await reader.ReadToEndAsync().ConfigureAwait(false));
}
[Fact]
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
new file mode 100644
index 000000000..5d7b0e874
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Dlna;
+using Xunit;
+using Xunit.Priority;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
+ public sealed class DlnaControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private const string NonExistentProfile = "1322f35b8f2c434dad3cc07c9b97dbd1";
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+ private static string? _newDeviceProfileId;
+
+ public DlnaControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task GetProfile_DoesNotExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var getResponse = await client.GetAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task DeleteProfile_DoesNotExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var getResponse = await client.DeleteAsync("/Dlna/Profiles/" + NonExistentProfile).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(0)]
+ public async Task UpdateProfile_DoesNotExist_NotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var deviceProfile = new DeviceProfile()
+ {
+ Name = "ThisProfileDoesNotExist"
+ };
+
+ using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(1)]
+ public async Task CreateProfile_Valid_NoContent()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var deviceProfile = new DeviceProfile()
+ {
+ Name = "ThisProfileIsNew"
+ };
+
+ using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(2)]
+ public async Task GetProfileInfos_Valid_ContainsThisProfileIsNew()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var response = await client.GetAsync("/Dlna/ProfileInfos").ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+
+ var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ _jsonOptions).ConfigureAwait(false);
+
+ var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal));
+ Assert.NotNull(newProfile);
+ _newDeviceProfileId = newProfile!.Id;
+ }
+
+ [Fact]
+ [Priority(3)]
+ public async Task UpdateProfile_Valid_NoContent()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var updatedProfile = new DeviceProfile()
+ {
+ Name = "ThisProfileIsUpdated",
+ Id = _newDeviceProfileId
+ };
+
+ using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles", updatedProfile, _jsonOptions).ConfigureAwait(false);
+ Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
+ }
+
+ [Fact]
+ [Priority(4)]
+ public async Task DeleteProfile_Valid_NoContent()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var getResponse = await client.DeleteAsync("/Dlna/Profiles/" + _newDeviceProfileId).ConfigureAwait(false);
+ Console.WriteLine(await getResponse.Content.ReadAsStringAsync().ConfigureAwait(false));
+ Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs
new file mode 100644
index 000000000..34d26680a
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs
@@ -0,0 +1,61 @@
+using System.Globalization;
+using System.Net;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ public sealed class MediaInfoControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public MediaInfoControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task BitrateTest_Default_Ok()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("Playback/BitrateTest").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType);
+ Assert.NotNull(response.Content.Headers.ContentLength);
+ }
+
+ [Theory]
+ [InlineData(102400)]
+ public async Task BitrateTest_WithValidParam_Ok(int size)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType);
+ Assert.NotNull(response.Content.Headers.ContentLength);
+ Assert.InRange(response.Content.Headers.ContentLength!.Value, size, long.MaxValue);
+ }
+
+ [Theory]
+ [InlineData(0)] // Zero
+ [InlineData(-102400)] // Negative value
+ [InlineData(1000000000)] // Too large
+ public async Task BitrateTest_InvalidValue_BadRequest(int size)
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
new file mode 100644
index 000000000..24251013c
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Api.Models.LibraryStructureDto;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Configuration;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers
+{
+ public sealed class MediaStructureControllerTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public MediaStructureControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task RenameVirtualFolder_WhiteSpaceName_ReturnsBadRequest()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var postContent = new ByteArrayContent(Array.Empty<byte>());
+ var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task RenameVirtualFolder_WhiteSpaceNewName_ReturnsBadRequest()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var postContent = new ByteArrayContent(Array.Empty<byte>());
+ var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task RenameVirtualFolder_NameDoesntExist_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ using var postContent = new ByteArrayContent(Array.Empty<byte>());
+ var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddMediaPath_PathDoesntExist_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var data = new MediaPathDto()
+ {
+ Name = "Test",
+ Path = "/this/path/doesnt/exist"
+ };
+
+ var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task UpdateMediaPath_WhiteSpaceName_ReturnsBadRequest()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var data = new UpdateMediaPathRequestDto()
+ {
+ Name = " ",
+ PathInfo = new MediaPathInfo("test")
+ };
+
+ var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions).ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task RemoveMediaPath_WhiteSpaceName_ReturnsBadRequest()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task RemoveMediaPath_PathDoesntExist_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client).ConfigureAwait(false));
+
+ var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
index 169a5a6c5..e72dacfe0 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
@@ -1,12 +1,12 @@
using System;
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
+using System.Net.Http.Json;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StartupDtos;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using Xunit;
using Xunit.Priority;
@@ -36,9 +36,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
PreferredMetadataLanguage = "nl"
};
- using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(config, _jsonOptions));
- postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- using var postResponse = await client.PostAsync("/Startup/Configuration", postContent).ConfigureAwait(false);
+ using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
using var getResponse = await client.GetAsync("/Startup/Configuration").ConfigureAwait(false);
@@ -80,9 +78,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Password = "NewPassword"
};
- using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions));
- postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- var postResponse = await client.PostAsync("/Startup/User", postContent).ConfigureAwait(false);
+ var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
var getResponse = await client.GetAsync("/Startup/User").ConfigureAwait(false);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
index 6584490de..588e25a82 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -3,12 +3,11 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Net.Mime;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Models.UserDtos;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Model.Dto;
using Xunit;
using Xunit.Priority;
@@ -31,18 +30,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
}
private Task<HttpResponseMessage> CreateUserByName(HttpClient httpClient, CreateUserByName request)
- {
- using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
- postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- return httpClient.PostAsync("Users/New", postContent);
- }
+ => httpClient.PostAsJsonAsync("Users/New", request, _jsonOpions);
private Task<HttpResponseMessage> UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request)
- {
- using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(request, _jsonOpions));
- postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- return httpClient.PostAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", postContent);
- }
+ => httpClient.PostAsJsonAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", request, _jsonOpions);
[Fact]
[Priority(-1)]
diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
index 732b4f050..2361e4aa4 100644
--- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs
@@ -21,6 +21,7 @@ namespace Jellyfin.Server.Integration.Tests
[InlineData("a=1", "a=1")] // won't be processed as it has a value
[InlineData("a%3D1%26b%3D2%26c%3D3", "a=1&b=2&c=3")] // will be processed.
[InlineData("a=b&a=c", "a=b")]
+ [InlineData("a%3D1", "a=1")]
[InlineData("a%3Db%26a%3Dc", "a=b")]
public async Task Ensure_Decoding_Of_Urls_Is_Working(string sourceUrl, string unencodedUrl)
{
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 59f125cd0..a59900b02 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -1,10 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -12,14 +9,14 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index d9ec81a27..3d34a18e7 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -3,7 +3,6 @@ using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
-using Emby.Server.Implementations.IO;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
@@ -44,10 +43,7 @@ namespace Jellyfin.Server.Integration.Tests
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Specify the startup command line options
- var commandLineOpts = new StartupOptions
- {
- NoWebClient = true
- };
+ var commandLineOpts = new StartupOptions();
// Use a temporary directory for the application paths
var webHostPathRoot = Path.Combine(_testPathRoot, "test-host-" + Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
@@ -70,7 +66,7 @@ namespace Jellyfin.Server.Integration.Tests
var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths);
ILoggerFactory loggerFactory = new SerilogLoggerFactory();
- var serviceCollection = new ServiceCollection();
+
_disposableComponents.Add(loggerFactory);
// Create the app host and initialize it
@@ -78,11 +74,10 @@ namespace Jellyfin.Server.Integration.Tests
appPaths,
loggerFactory,
commandLineOpts,
- new ConfigurationBuilder().Build(),
- new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
- serviceCollection);
+ new ConfigurationBuilder().Build());
_disposableComponents.Add(appHost);
- appHost.Init();
+ var serviceCollection = new ServiceCollection();
+ appHost.Init(serviceCollection);
// Configure the web host builder
Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs
new file mode 100644
index 000000000..8c49a2e2b
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs
@@ -0,0 +1,32 @@
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Middleware
+{
+ public sealed class RobotsRedirectionMiddlewareTests : IClassFixture<JellyfinApplicationFactory>
+ {
+ private readonly JellyfinApplicationFactory _factory;
+
+ public RobotsRedirectionMiddlewareTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task RobotsDotTxtRedirects()
+ {
+ var client = _factory.CreateClient(
+ new WebApplicationFactoryClientOptions()
+ {
+ AllowAutoRedirect = false
+ });
+
+ var response = await client.GetAsync("robots.txt").ConfigureAwait(false);
+
+ Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
+ Assert.Equal("web/robots.txt", response.Headers.Location?.ToString());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
index 0a463cfa3..bf74efa09 100644
--- a/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/TestAppHost.cs
@@ -2,9 +2,7 @@ using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using MediaBrowser.Controller;
-using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Integration.Tests
@@ -21,22 +19,16 @@ namespace Jellyfin.Server.Integration.Tests
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
- /// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public TestAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
- IConfiguration startup,
- IFileSystem fileSystem,
- IServiceCollection collection)
+ IConfiguration startup)
: base(
applicationPaths,
loggerFactory,
options,
- startup,
- fileSystem,
- collection)
+ startup)
{
}
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index c8e72c10d..ada9034df 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -1,11 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -13,13 +10,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.7" />
- <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
- <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
+ <PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index 146b16cf9..a1bdfa31b 100644
--- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -1,10 +1,12 @@
-using System.Globalization;
-using System.Text;
+using System;
+using System.Linq;
+using System.Net;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -13,20 +15,63 @@ namespace Jellyfin.Server.Tests
{
public class ParseNetworkTests
{
- /// <summary>
- /// Order of the result has always got to be hosts, then networks.
- /// </summary>
- /// <param name="ip4">IP4 enabled.</param>
- /// <param name="ip6">IP6 enabled.</param>
- /// <param name="hostList">List to parse.</param>
- /// <param name="match">What it should match.</param>
+ public static TheoryData<bool, bool, string[], IPAddress[], IPNetwork[]> TestNetworks_TestData()
+ {
+ var data = new TheoryData<bool, bool, string[], IPAddress[], IPNetwork[]>();
+ data.Add(
+ true,
+ true,
+ new string[] { "192.168.t", "127.0.0.1", "1234.1232.12.1234" },
+ new IPAddress[] { IPAddress.Loopback.MapToIPv6() },
+ Array.Empty<IPNetwork>());
+
+ data.Add(
+ true,
+ false,
+ new string[] { "192.168.x", "127.0.0.1", "1234.1232.12.1234" },
+ new IPAddress[] { IPAddress.Loopback },
+ Array.Empty<IPNetwork>());
+
+ data.Add(
+ true,
+ true,
+ new string[] { "::1" },
+ Array.Empty<IPAddress>(),
+ new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
+
+ data.Add(
+ false,
+ false,
+ new string[] { "localhost" },
+ Array.Empty<IPAddress>(),
+ Array.Empty<IPNetwork>());
+
+ data.Add(
+ true,
+ false,
+ new string[] { "localhost" },
+ new IPAddress[] { IPAddress.Loopback },
+ Array.Empty<IPNetwork>());
+
+ data.Add(
+ false,
+ true,
+ new string[] { "localhost" },
+ Array.Empty<IPAddress>(),
+ new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
+
+ data.Add(
+ true,
+ true,
+ new string[] { "localhost" },
+ new IPAddress[] { IPAddress.Loopback.MapToIPv6() },
+ new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
+ return data;
+ }
+
[Theory]
- // [InlineData(true, true, "192.168.0.0/16,www.yahoo.co.uk", "::ffff:212.82.100.150,::ffff:192.168.0.0/16")] <- fails on Max. www.yahoo.co.uk resolves to a different ip address.
- // [InlineData(true, false, "192.168.0.0/16,www.yahoo.co.uk", "212.82.100.150,192.168.0.0/16")]
- [InlineData(true, true, "192.168.t,127.0.0.1,1234.1232.12.1234", "::ffff:127.0.0.1")]
- [InlineData(true, false, "192.168.x,127.0.0.1,1234.1232.12.1234", "127.0.0.1")]
- [InlineData(true, true, "::1", "::1/128")]
- public void TestNetworks(bool ip4, bool ip6, string hostList, string match)
+ [MemberData(nameof(TestNetworks_TestData))]
+ public void TestNetworks(bool ip4, bool ip6, string[] hostList, IPAddress[] knownProxies, IPNetwork[] knownNetworks)
{
using var nm = CreateNetworkManager();
@@ -36,31 +81,25 @@ namespace Jellyfin.Server.Tests
EnableIPV6 = ip6
};
- var result = match + ",";
ForwardedHeadersOptions options = new ForwardedHeadersOptions();
// Need this here as ::1 and 127.0.0.1 are in them by default.
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
- ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(','), options);
+ ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList, options);
- var sb = new StringBuilder();
- foreach (var item in options.KnownProxies)
+ Assert.Equal(knownProxies.Length, options.KnownProxies.Count);
+ foreach (var item in knownProxies)
{
- sb.Append(item)
- .Append(',');
+ Assert.True(options.KnownProxies.Contains(item));
}
- foreach (var item in options.KnownNetworks)
+ Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count);
+ foreach (var item in knownNetworks)
{
- sb.Append(item.Prefix)
- .Append('/')
- .Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture))
- .Append(',');
+ Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.Prefix) && x.PrefixLength == item.PrefixLength));
}
-
- Assert.Equal(sb.ToString(), result);
}
private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
diff --git a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
index 419afb2dc..d15c9d6f5 100644
--- a/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
+++ b/tests/Jellyfin.Server.Tests/UrlDecodeQueryFeatureTests.cs
@@ -12,9 +12,6 @@ namespace Jellyfin.Server.Tests
{
[Theory]
[InlineData("e0a72cb2a2c7", "e0a72cb2a2c7")] // isn't encoded
- [InlineData("random+test", "random test")] // encoded
- [InlineData("random%20test", "random test")] // encoded
- [InlineData("++", " ")] // encoded
public static void EmptyValueTest(string query, string key)
{
var dict = new Dictionary<string, StringValues>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index 0a04a5c54..edf9e0fef 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -1,11 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
- <Nullable>enable</Nullable>
- <AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -16,11 +13,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="3.0.3" />
+ <PackageReference Include="coverlet.collector" Version="3.1.0" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
index 357d61c0b..8019e0ab3 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Location/MovieNfoLocationTests.cs
@@ -1,8 +1,8 @@
-using System.Linq;
+using System;
+using System.Linq;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.System;
using MediaBrowser.XbmcMetadata.Savers;
using Xunit;
@@ -28,7 +28,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
var path2 = "/media/movies/Avengers Endgame/movie.nfo";
// uses ContainingFolderPath which uses Operating system specific paths
- if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ if (OperatingSystem.IsWindows())
{
movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\');
@@ -49,7 +49,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
var path2 = "/media/movies/Avengers Endgame/VIDEO_TS/VIDEO_TS.nfo";
// uses ContainingFolderPath which uses Operating system specific paths
- if (MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows)
+ if (OperatingSystem.IsWindows())
{
movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\');
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 30a48857a..7ea45d14d 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.System;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging.Abstractions;
@@ -59,7 +58,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
_localImageFileMetadata = new FileSystemMetadata()
{
Exists = true,
- FullName = MediaBrowser.Common.System.OperatingSystem.Id == OperatingSystemId.Windows ?
+ FullName = OperatingSystem.IsWindows() ?
"C:\\media\\movies\\Justice League (2017).jpg"
: "/media/movies/Justice League (2017).jpg"
};
@@ -208,6 +207,20 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
}
[Fact]
+ public void Parse_GivenFileWithFanartTag_Success()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new Movie()
+ };
+
+ _parser.Fetch(result, "Test Data/Fanart.nfo", CancellationToken.None);
+
+ Assert.Single(result.RemoteImages.Where(x => x.type == ImageType.Backdrop));
+ Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg", result.RemoteImages.First(x => x.type == ImageType.Backdrop).url);
+ }
+
+ [Fact]
public void Parse_RadarrUrlFile_Success()
{
var result = new MetadataResult<Video>()
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Fanart.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Fanart.nfo
new file mode 100644
index 000000000..0b129bd8c
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Fanart.nfo
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<movie>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57b476a831d74.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57b476a831d74.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57947e28cf10b.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57947e28cf10b.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5863d5c0cf0c9.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5863d5c0cf0c9.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5a801747e5545.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5a801747e5545.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5cd75683df92b.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5cd75683df92b.png</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-586017e95adbd.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-586017e95adbd.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-5934d45bc6592.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-5934d45bc6592.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-5aa9289a379fa.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-5aa9289a379fa.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-585fb155c3743.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585fb155c3743.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-585edbda91d82.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585edbda91d82.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-5b86588882c12.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-5b86588882c12.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-5bbb9babe600c.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-5bbb9babe600c.jpg</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/movies/141052/hdmovieclearart/justice-league-5865c23193041.png">https://assets.fanart.tv/fanart/movies/141052/hdmovieclearart/justice-league-5865c23193041.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a3af26360617.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a3af26360617.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-58690967b9765.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-58690967b9765.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a953ca4db6a6.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a953ca4db6a6.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a0b913c233be.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a0b913c233be.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-59dc595362ef1.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-59dc595362ef1.png</thumb>
+ <fanart>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-58fa1f1932897.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-58fa1f1932897.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a14f5fd8dd16.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a14f5fd8dd16.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a119394ea362.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a119394ea362.jpg</thumb>
+ </fanart>
+ <thumb aspect="fanart">This-should-not-be-saved-as-a-fanart-image.jpg</thumb>
+</movie>