aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml22
l---------.copr1
l---------.copr/Makefile1
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs56
-rw-r--r--Emby.Dlna/DlnaManager.cs3
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs4
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs5
-rw-r--r--Emby.Naming/AudioBook/AudioBookListResolver.cs10
-rw-r--r--Emby.Naming/AudioBook/AudioBookResolver.cs4
-rw-r--r--Emby.Naming/Common/NamingOptions.cs53
-rw-r--r--Emby.Naming/Emby.Naming.csproj2
-rw-r--r--Emby.Naming/Subtitles/SubtitleParser.cs11
-rw-r--r--Emby.Naming/TV/EpisodeResolver.cs4
-rw-r--r--Emby.Naming/TV/SeriesPathParser.cs1
-rw-r--r--Emby.Naming/Video/CleanStringParser.cs1
-rw-r--r--Emby.Naming/Video/ExtraResolver.cs103
-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.cs7
-rw-r--r--Emby.Notifications/NotificationEntryPoint.cs3
-rw-r--r--Emby.Photos/PhotoProvider.cs3
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs2
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs1
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs152
-rw-r--r--Emby.Server.Implementations/Archiving/ZipClient.cs116
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs17
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs3
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs90
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs4
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs2
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs250
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs12
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj4
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs7
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs3
-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.cs6
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs16
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs4
-rw-r--r--Emby.Server.Implementations/Images/MusicGenreImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs31
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs201
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs15
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs4
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs30
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs67
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs30
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs21
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs130
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs3
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs126
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs11
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs52
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs30
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs66
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs49
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs24
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs3
-rw-r--r--Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs156
-rw-r--r--Emby.Server.Implementations/Library/Validators/PeopleValidator.cs3
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs23
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs47
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs8
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs12
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs30
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json4
-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/ca.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json46
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/mn.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json48
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/te.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zu.json29
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs119
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs5
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs3
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/ArtistComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs8
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs6
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs9
-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/CustomAuthenticationHandler.cs5
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ClientLogController.cs77
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs13
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs5
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs14
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs6
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs36
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs51
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs28
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs14
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs9
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs2
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs6
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs6
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs7
-rw-r--r--Jellyfin.Api/Helpers/ClassMigrationHelper.cs71
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs7
-rw-r--r--Jellyfin.Api/Helpers/ProgressiveFileStream.cs76
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs16
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs1
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs30
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs2
-rw-r--r--Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs2
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj2
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs6
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs24
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs3
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj10
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs18
-rw-r--r--Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs35
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs8
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs12
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj11
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs65
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs138
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs3
-rw-r--r--Jellyfin.Server/Program.cs49
-rw-r--r--MediaBrowser.Common/Cryptography/CryptoExtensions.cs35
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs7
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj2
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs14
-rw-r--r--MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs26
-rw-r--r--MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs8
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs19
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs3
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs354
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs10
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs4
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs68
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs71
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs14
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs22
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs7
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs73
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs32
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs17
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs46
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs35
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs11
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerInfo.cs12
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs27
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs5
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs4
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs4
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs3
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PausedGroupState.cs6
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/PlayingGroupState.cs6
-rw-r--r--MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs5
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs15
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs36
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj4
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs64
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs5
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs224
-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/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.cs15
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs12
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs20
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs7
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleProfile.cs4
-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.cs7
-rw-r--r--MediaBrowser.Model/IO/IZipClient.cs56
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj8
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs151
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs8
-rw-r--r--MediaBrowser.Model/Querying/LatestItemsQuery.cs3
-rw-r--r--MediaBrowser.Model/Search/SearchQuery.cs9
-rw-r--r--MediaBrowser.Model/Session/BrowseRequest.cs5
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs3
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs5
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs12
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs10
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs40
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioResolver.cs176
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs48
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs74
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs32
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs54
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs3
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs19
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs37
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs54
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs219
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs132
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs41
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html64
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs68
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs4
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs7
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs8
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs3
-rw-r--r--RSSDP/HttpParserBase.cs6
-rw-r--r--RSSDP/HttpRequestParser.cs4
-rw-r--r--RSSDP/HttpResponseParser.cs4
-rw-r--r--RSSDP/SsdpDevicePublisher.cs15
-rwxr-xr-xbump_version22
-rw-r--r--debian/jellyfin.service14
-rw-r--r--deployment/Dockerfile.centos.amd642
-rw-r--r--deployment/Dockerfile.fedora.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--fedora/Makefile75
-rw-r--r--fedora/jellyfin-server-lowports.conf4
-rw-r--r--fedora/jellyfin.spec21
-rw-r--r--jellyfin.ruleset10
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs8
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs18
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs11
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs20
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs13
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs2
-rw-r--r--tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs30
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj2
-rw-r--r--tests/Jellyfin.Controller.Tests/BaseItemManagerTests.cs88
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs13
-rw-r--r--tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs (renamed from tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs)4
-rw-r--r--tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs159
-rw-r--r--tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs11
-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.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs268
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs31
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs160
-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/PathExtensionsTests.cs12
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs7
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs15
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs11
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs10
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs15
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj2
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj2
340 files changed, 4457 insertions, 4539 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index e227d5fe6..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'
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/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 5c4a031bc..d52e13324 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -150,6 +150,7 @@
- [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
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 657850ac0..fde3f2f89 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -18,11 +18,8 @@ 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;
@@ -30,12 +27,7 @@ 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
{
@@ -395,6 +387,7 @@ namespace Emby.Dlna.ContentDirectory
}
writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -484,6 +477,7 @@ namespace Emby.Dlna.ContentDirectory
}
writer.WriteFullEndElement();
+ writer.Flush();
xmlWriter.WriteElementString("Result", builder.ToString());
}
@@ -537,7 +531,7 @@ namespace Emby.Dlna.ContentDirectory
User = user,
Recursive = true,
IsMissing = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsFolder = isFolder,
MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions()
@@ -617,7 +611,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit,
StartIndex = startIndex,
IsVirtualItem = false,
- ExcludeItemTypes = new[] { nameof(Book) },
+ ExcludeItemTypes = new[] { BaseItemKind.Book },
IsPlaceHolder = false,
DtoOptions = GetDtoOptions(),
OrderBy = GetOrderBy(sort, folder.IsPreSorted)
@@ -642,7 +636,7 @@ namespace Emby.Dlna.ContentDirectory
{
StartIndex = startIndex,
Limit = limit,
- IncludeItemTypes = new[] { nameof(LiveTvChannel) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
OrderBy = GetOrderBy(sort, false)
};
@@ -673,23 +667,23 @@ namespace Emby.Dlna.ContentDirectory
switch (stubType)
{
case StubType.Latest:
- return GetLatest(item, query, nameof(Audio));
+ return GetLatest(item, query, BaseItemKind.Audio);
case StubType.Playlists:
return GetMusicPlaylists(query);
case StubType.Albums:
- return GetChildrenOfItem(item, query, nameof(MusicAlbum));
+ 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, nameof(MusicAlbum), true);
+ return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
case StubType.FavoriteArtists:
return GetFavoriteArtists(item, query);
case StubType.FavoriteSongs:
- return GetChildrenOfItem(item, query, nameof(Audio), true);
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
case StubType.Songs:
- return GetChildrenOfItem(item, query, nameof(Audio));
+ return GetChildrenOfItem(item, query, BaseItemKind.Audio);
case StubType.Genres:
return GetMusicGenres(item, query);
}
@@ -744,13 +738,13 @@ namespace Emby.Dlna.ContentDirectory
case StubType.ContinueWatching:
return GetMovieContinueWatching(item, query);
case StubType.Latest:
- return GetLatest(item, query, nameof(Movie));
+ return GetLatest(item, query, BaseItemKind.Movie);
case StubType.Movies:
- return GetChildrenOfItem(item, query, nameof(Movie));
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie);
case StubType.Collections:
return GetMovieCollections(query);
case StubType.Favorites:
- return GetChildrenOfItem(item, query, nameof(Movie), true);
+ return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
case StubType.Genres:
return GetGenres(item, query);
}
@@ -829,13 +823,13 @@ namespace Emby.Dlna.ContentDirectory
case StubType.NextUp:
return GetNextUp(item, query);
case StubType.Latest:
- return GetLatest(item, query, nameof(Episode));
+ return GetLatest(item, query, BaseItemKind.Episode);
case StubType.Series:
- return GetChildrenOfItem(item, query, nameof(Series));
+ return GetChildrenOfItem(item, query, BaseItemKind.Series);
case StubType.FavoriteSeries:
- return GetChildrenOfItem(item, query, nameof(Series), true);
+ return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
case StubType.FavoriteEpisodes:
- return GetChildrenOfItem(item, query, nameof(Episode), true);
+ return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
case StubType.Genres:
return GetGenres(item, query);
}
@@ -896,7 +890,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
{
query.Recursive = true;
- query.IncludeItemTypes = new[] { nameof(BoxSet) };
+ query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
var result = _libraryManager.GetItemsResult(query);
@@ -911,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
/// <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> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, string itemType, bool isFavorite = false)
+ private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
{
query.Recursive = true;
query.Parent = parent;
@@ -1011,7 +1005,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { nameof(Playlist) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
query.Recursive = true;
var result = _libraryManager.GetItemsResult(query);
@@ -1050,7 +1044,7 @@ namespace Emby.Dlna.ContentDirectory
/// <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> GetLatest(BaseItem parent, InternalItemsQuery query, string itemType)
+ private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
@@ -1084,7 +1078,7 @@ namespace Emby.Dlna.ContentDirectory
{
Recursive = true,
ArtistIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions(),
@@ -1113,8 +1107,8 @@ namespace Emby.Dlna.ContentDirectory
GenreIds = new[] { item.Id },
IncludeItemTypes = new[]
{
- nameof(Movie),
- nameof(Series)
+ BaseItemKind.Movie,
+ BaseItemKind.Series
},
Limit = limit,
StartIndex = startIndex,
@@ -1142,7 +1136,7 @@ namespace Emby.Dlna.ContentDirectory
{
Recursive = true,
GenreIds = new[] { item.Id },
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions(),
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 277a0e678..d9d2a345a 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -5,7 +5,6 @@ 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;
@@ -350,7 +349,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(systemProfilesPath);
var fileOptions = AsyncFile.WriteOptions;
- fileOptions.Mode = FileMode.CreateNew;
+ fileOptions.Mode = FileMode.Create;
fileOptions.PreallocationSize = length;
using (var fileStream = new FileStream(path, fileOptions))
{
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 722428c73..08f639d93 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -124,7 +124,7 @@ namespace Emby.Dlna.Main
config);
Current = this;
- var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
+ var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
@@ -262,7 +262,6 @@ namespace Emby.Dlna.Main
{
_publisher = new SsdpDevicePublisher(
_communicationsServer,
- _networkManager,
MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
@@ -400,7 +399,6 @@ namespace Emby.Dlna.Main
_imageProcessor,
_deviceDiscovery,
_httpClientFactory,
- _config,
_userDataManager,
_localization,
_mediaSourceManager,
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 7927f5f8f..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;
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index 1e4a8d2ed..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();
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 7bc9fbce8..aa62a47f1 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -1,6 +1,7 @@
#pragma warning disable CA1819
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
@@ -124,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[]
@@ -405,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),
@@ -470,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",
@@ -563,7 +576,7 @@ namespace Emby.Naming.Common
ExtraType.Unknown,
ExtraRuleType.DirectoryName,
"extras",
- MediaType.Video),
+ MediaType.Video)
};
Format3DRules = new[]
@@ -675,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; }
@@ -759,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.
@@ -784,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>();
@@ -813,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 4c5dcdafc..2bf8eacb1 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -38,7 +38,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Code Analyzers-->
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/SeriesPathParser.cs b/Emby.Naming/TV/SeriesPathParser.cs
index a62e5f4d6..4dfbb36a3 100644
--- a/Emby.Naming/TV/SeriesPathParser.cs
+++ b/Emby.Naming/TV/SeriesPathParser.cs
@@ -1,4 +1,3 @@
-using System.Globalization;
using Emby.Naming.Common;
namespace Emby.Naming.TV
diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs
index b81333500..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;
diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs
index 7bc226614..fbdca859f 100644
--- a/Emby.Naming/Video/ExtraResolver.cs
+++ b/Emby.Naming/Video/ExtraResolver.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Audio;
using Emby.Naming.Common;
@@ -9,45 +11,27 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolve if file is extra for video.
/// </summary>
- public class ExtraResolver
+ public static class ExtraResolver
{
- private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
- 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();
@@ -76,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;
@@ -102,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 ed7d511a3..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(IEnumerable<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 4c9df27f5..9cadc1465 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -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);
diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs
index e8ae14ff2..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;
@@ -104,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;
}
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 3a916e5c0..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
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 73919f306..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;
@@ -118,7 +120,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// The disposable parts.
/// </summary>
- private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
+ private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new ();
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
@@ -129,7 +131,6 @@ namespace Emby.Server.Implementations
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private string[] _urlPrefixes;
/// <summary>
/// Gets or sets all concrete types.
@@ -210,7 +211,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
- public INetworkManager NetManager { get; internal set; }
+ public INetworkManager NetManager { get; private set; }
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
@@ -232,16 +233,16 @@ namespace Emby.Server.Implementations
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 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.
@@ -312,22 +313,6 @@ namespace Emby.Server.Implementations
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
- /// <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;
@@ -345,22 +330,6 @@ namespace Emby.Server.Implementations
}
/// <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);
-
- /// <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);
-
- /// <summary>
/// Creates the instance safe.
/// </summary>
/// <param name="type">The type.</param>
@@ -369,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)
@@ -379,7 +348,7 @@ namespace Emby.Server.Implementations
_pluginManager.FailPlugin(type.Assembly);
- throw new ExternalException("DI Loop detected.");
+ throw new TypeLoadException("DI Loop detected");
}
try
@@ -412,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 />
@@ -428,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);
}
}
@@ -449,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);
}
}
@@ -521,8 +497,6 @@ namespace Emby.Server.Implementations
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
@@ -563,7 +537,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
serviceCollection.AddSingleton<IApplicationHost>(this);
- serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
+ serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(_fileSystemManager);
@@ -586,7 +560,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IZipClient, ZipClient>();
serviceCollection.AddSingleton<IServerApplicationHost>(this);
- serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+ serviceCollection.AddSingleton(ApplicationPaths);
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
@@ -605,6 +579,7 @@ namespace Emby.Server.Implementations
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>();
@@ -790,8 +765,6 @@ namespace Emby.Server.Implementations
_pluginManager.CreatePlugins();
- _urlPrefixes = GetUrlPrefixes().ToArray();
-
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(),
@@ -859,32 +832,12 @@ namespace Emby.Server.Implementations
}
}
- private IEnumerable<string> GetUrlPrefixes()
- {
- var hosts = new[] { "+" };
-
- return hosts.SelectMany(i =>
- {
- var prefixes = new List<string>
- {
- "http://" + i + ":" + HttpPort + "/"
- };
-
- if (Certificate != 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();
@@ -893,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)
{
@@ -906,11 +859,6 @@ namespace Emby.Server.Implementations
}
}
- if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
- {
- requiresRestart = true;
- }
-
if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
@@ -952,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()
{
@@ -1093,11 +1041,6 @@ namespace Emby.Server.Implementations
};
}
- public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
- => NetManager.GetMacAddresses()
- .Select(i => new WakeOnLanInfo(i))
- .ToList();
-
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
@@ -1113,7 +1056,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
+ public string GetSmartApiUrl(IPAddress remoteAddr)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1122,12 +1065,12 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(remoteAddr, out port);
+ 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)
@@ -1148,12 +1091,12 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(request, out port);
+ 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))
@@ -1162,17 +1105,17 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(hostname, out port);
+ string smart = NetManager.GetBindInterface(hostname, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
/// <inheritdoc/>
- public string GetApiUrlForLocalAccess(bool allowHttps)
+ public string GetApiUrlForLocalAccess(bool allowHttps = true)
{
// With an empty source, the port will be null
string smart = NetManager.GetBindInterface(string.Empty, out _);
- var scheme = allowHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
- var port = allowHttps ? HttpsPort : HttpPort;
+ var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
+ int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
}
@@ -1258,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
{
@@ -1271,9 +1217,11 @@ 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;
diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs
index 9e1d550eb..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,55 +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;
- }
-
- Directory.CreateDirectory(targetPath);
- 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
- };
-
- Directory.CreateDirectory(targetPath);
- reader.WriteAllToDirectory(targetPath, options);
- }
-
/// <inheritdoc />
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
{
@@ -94,69 +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
- };
-
- Directory.CreateDirectory(targetPath);
- 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
- };
-
- Directory.CreateDirectory(targetPath);
- reader.WriteAllToDirectory(targetPath, options);
- }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 09aee602a..8702691d1 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -10,6 +10,7 @@ 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.Progress;
@@ -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();
}
@@ -1075,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;
@@ -1143,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);
@@ -1152,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/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index 673810c49..e9c005cea 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,9 +1,11 @@
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
{
@@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
/// </summary>
public class CryptographyProvider : ICryptoProvider
{
- // 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
+ // TODO: remove when not needed for backwards compat
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{
"MD5",
@@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
};
/// <inheritdoc />
- public string DefaultHashMethod => "PBKDF2";
+ public string DefaultHashMethod => "PBKDF2-SHA512";
/// <inheritdoc />
- public IEnumerable<string> GetSupportedHashMethods()
- => _supportedHashMethods;
-
- private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
+ public PasswordHash CreatePasswordHash(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)
- {
- throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
- }
-
- using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
- return r.GetBytes(32);
+ 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 byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
+ public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
{
- if (hashMethod == DefaultHashMethod)
+ if (string.Equals(hash.Id, "PBKDF2", 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.SHA1,
+ 32));
}
- if (!_supportedHashMethods.Contains(hashMethod))
+ if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
{
- throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+ return hash.Hash.SequenceEqual(
+ Rfc2898DeriveBytes.Pbkdf2(
+ password,
+ hash.Salt,
+ int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+ HashAlgorithmName.SHA512,
+ DefaultOutputLength));
}
- using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
- if (salt.Length == 0)
+ if (!_supportedHashMethods.Contains(hash.Id))
{
- return h.ComputeHash(bytes);
+ throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
}
- byte[] salted = new byte[bytes.Length + salt.Length];
+ 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 hash.Hash.SequenceEqual(h.ComputeHash(bytes));
+ }
+
+ 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)
- => RandomNumberGenerator.GetBytes(length);
+ {
+ var salt = new byte[length];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetNonZeroBytes(salt);
+ return salt;
+ }
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 73c31f49d..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;
@@ -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;
}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
index 44dad5b17..11e33278d 100644
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ b/Emby.Server.Implementations/Data/ManagedConnection.cs
@@ -7,7 +7,7 @@ using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
- public class ManagedConnection : IDisposable
+ public sealed class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim _writeLock;
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 13f1df7c8..beae7e243 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Data
private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
- private static readonly string[] _retriveItemColumns =
+ private static readonly string[] _retrieveItemColumns =
{
"type",
"data",
@@ -133,7 +133,7 @@ namespace Emby.Server.Implementations.Data
"OwnerId"
};
- private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+ private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns =
{
@@ -196,57 +196,56 @@ namespace Emby.Server.Implementations.Data
private static readonly string _mediaAttachmentInsertPrefix;
- private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _programTypes = new[]
{
- "Program",
- "TvChannel",
- "LiveTvProgram",
- "LiveTvTvChannel"
+ BaseItemKind.Program,
+ BaseItemKind.TvChannel,
+ BaseItemKind.LiveTvProgram,
+ BaseItemKind.LiveTvChannel
};
- private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
{
- "Series",
- "Season",
- "MusicAlbum",
- "MusicArtist",
- "PhotoAlbum"
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.PhotoAlbum
};
- private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _serviceTypes = new[]
{
- "TvChannel",
- "LiveTvTvChannel"
+ BaseItemKind.TvChannel,
+ BaseItemKind.LiveTvChannel
};
- private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _startDateTypes = new[]
{
- "Program",
- "LiveTvProgram"
+ BaseItemKind.Program,
+ BaseItemKind.LiveTvProgram
};
- private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _seriesTypes = new[]
{
- "Book",
- "AudioBook",
- "Episode",
- "Season"
+ BaseItemKind.Book,
+ BaseItemKind.AudioBook,
+ BaseItemKind.Episode,
+ BaseItemKind.Season
};
- private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
{
- "Series",
- "Season",
- "PhotoAlbum"
+ BaseItemKind.Series,
+ BaseItemKind.Season,
+ BaseItemKind.PhotoAlbum
};
- private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly BaseItemKind[] _artistsTypes = new[]
{
- "Audio",
- "MusicAlbum",
- "MusicVideo",
- "AudioBook",
- "AudioPodcast"
+ BaseItemKind.Audio,
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.AudioBook
};
private static readonly Type[] _knownTypes =
@@ -285,6 +284,43 @@ namespace Emby.Server.Implementations.Data
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();
@@ -1320,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);
@@ -2212,7 +2248,7 @@ namespace Emby.Server.Implementations.Data
private bool HasProgramAttributes(InternalItemsQuery query)
{
- if (_programExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2227,7 +2263,7 @@ namespace Emby.Server.Implementations.Data
private bool HasServiceName(InternalItemsQuery query)
{
- if (_programExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2242,7 +2278,7 @@ namespace Emby.Server.Implementations.Data
private bool HasStartDate(InternalItemsQuery query)
{
- if (_programExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2262,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)
@@ -2272,12 +2308,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
- return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
+ return query.IncludeItemTypes.Contains(BaseItemKind.Trailer);
}
private bool HasArtistFields(InternalItemsQuery query)
{
- if (_artistExcludeParentTypes.Contains(query.ParentType))
+ if (query.ParentType != null && _artistExcludeParentTypes.Contains(query.ParentType.Value))
{
return false;
}
@@ -2292,7 +2328,7 @@ namespace Emby.Server.Implementations.Data
private bool HasSeriesFields(InternalItemsQuery query)
{
- if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
+ if (query.ParentType == BaseItemKind.PhotoAlbum)
{
return false;
}
@@ -2630,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)
@@ -2821,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)
@@ -3487,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)");
}
@@ -3563,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)
@@ -3911,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)
{
@@ -4761,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);
}
@@ -4789,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)
@@ -4836,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;
}
@@ -4890,22 +4976,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return dict;
}
- 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)
@@ -5351,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))
{
@@ -5569,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/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index ab5d45279..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;
@@ -294,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;
}
@@ -370,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))
@@ -464,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
});
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 042b8f71a..329a84acb 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -29,10 +29,10 @@
<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.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.0" />
+ <PackageReference Include="sharpcompress" Version="0.30.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 640754af4..06e57ad12 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -13,7 +13,6 @@ 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;
@@ -27,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>();
@@ -42,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/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index e2ad07177..1d04f3da3 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -2,7 +2,6 @@
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (!auth.HasToken)
{
- throw new AuthenticationException("Request does not contain a token.");
+ return auth;
}
if (!auth.IsAuthenticated)
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index f86bfd755..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)
{
- _ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
+ 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 b525f5a2f..9fcc7fe59 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -449,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);
@@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
{
lock (_activeRefreshers)
{
+ refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose();
_activeRefreshers.Remove(refresher);
}
@@ -492,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/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 0229fbae7..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, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "Movie" };
+ includeItemTypes = new[] { BaseItemKind.Movie };
}
else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "Series" };
+ includeItemTypes = new[] { BaseItemKind.Series };
}
else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "MusicAlbum" };
+ includeItemTypes = new[] { BaseItemKind.MusicAlbum };
}
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, StringComparison.Ordinal))
{
- includeItemTypes = new string[] { "BoxSet" };
+ includeItemTypes = new[] { BaseItemKind.BoxSet };
}
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/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index 1f5090f7f..968bf5fa3 100644
--- a/Emby.Server.Implementations/Images/GenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -8,8 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -43,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/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
index baf1c9051..31f053f06 100644
--- a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
+++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
@@ -44,9 +44,9 @@ namespace Emby.Server.Implementations.Images
Genres = new[] { item.Name },
IncludeItemTypes = new[]
{
- nameof(MusicAlbum),
- nameof(MusicVideo),
- nameof(Audio)
+ BaseItemKind.MusicAlbum,
+ BaseItemKind.MusicVideo,
+ BaseItemKind.Audio
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4,
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index bc5b4499f..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.TrailersFolderName, 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;
}
@@ -78,7 +69,7 @@ namespace Emby.Server.Implementations.Library
{
// Don't resolve these into audio files
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
- && _libraryManager.IsAudioFile(filename))
+ && AudioFileParser.IsAudioFile(filename, _namingOptions))
{
return true;
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 559da7f5c..270264dba 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -11,11 +11,9 @@ 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;
@@ -79,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.
@@ -88,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>
@@ -116,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,
@@ -130,7 +127,8 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
- IMemoryCache memoryCache)
+ IMemoryCache memoryCache,
+ NamingOptions namingOptions)
{
_appHost = appHost;
_logger = logger;
@@ -146,6 +144,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
_memoryCache = memoryCache;
+ _namingOptions = namingOptions;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -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,
@@ -653,7 +652,7 @@ namespace Emby.Server.Implementations.Library
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);
@@ -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>()
@@ -2501,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;
@@ -2525,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;
@@ -2682,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
@@ -2706,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.TrailersFolderName, 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.ContainsKey(i.Name ?? string.Empty))
- .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)
@@ -2838,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);
@@ -2953,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;
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index b3837fedb..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
@@ -64,18 +65,18 @@ namespace Emby.Server.Implementations.Library
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 = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+ stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
}
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// 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.FirstOrDefault(s => !s.IsForced && 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)
@@ -136,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 e2f1fb0ad..d33213564 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -52,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>()
@@ -89,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 d5b855cdf..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;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index fd9747b4b..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 9e3f62276..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>
@@ -87,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <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>
@@ -101,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;
}
@@ -116,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
@@ -137,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}", 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 9ff99fa43..9222a9479 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -6,6 +6,7 @@ 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;
@@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
where T : Video, new()
{
- protected BaseVideoResolver(ILibraryManager libraryManager)
+ protected BaseVideoResolver(NamingOptions namingOptions)
{
- LibraryManager = libraryManager;
+ NamingOptions = namingOptions;
}
- protected ILibraryManager LibraryManager { get; }
+ protected NamingOptions NamingOptions { get; }
/// <summary>
/// Resolves the specified args.
@@ -48,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(filename))
+ 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);
+ }
+
+ Set3DFormat(video, videoInfo);
- return null;
+ return video;
}
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
@@ -206,8 +158,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
// 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))
{
- UdfReader udfReader = new UdfReader(videoFileStream);
if (udfReader.DirectoryExists("VIDEO_TS"))
{
video.IsoType = IsoType.Dvd;
@@ -267,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);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index e685c87f1..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
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
index 9599faea4..72341d9db 100644
--- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.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/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index e7abe1e6d..6cc04ea81 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -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 f3b6ef0a2..4feaf3fb4 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using Emby.Naming.Common;
using Emby.Naming.Video;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
@@ -38,10 +39,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <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))
{
@@ -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)
{
@@ -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 1c560e8a6..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;
@@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
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 8ce59717d..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;
@@ -57,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/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index cf2d22f4d..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.ContainsKey(parent.Name)))
+ 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 063f67543..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;
@@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
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 e62890083..f5ac3c665 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -5,7 +5,9 @@
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.Resolvers;
@@ -21,17 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
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>
@@ -54,12 +56,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
- var seriesInfo = Naming.TV.SeriesResolver.Resolve(_libraryManager.GetNamingOptions(), args.Path);
+ 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
@@ -91,7 +94,7 @@ 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
{
@@ -105,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)
@@ -118,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);
@@ -145,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;
}
@@ -181,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 id = justName.GetAttributeValue("tvdbid");
+ var tvdbId = justName.GetAttributeValue("tvdbid");
+ if (!string.IsNullOrEmpty(tvdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, 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 bebc25b77..4aacf7774 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -10,12 +10,9 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
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);
}
@@ -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/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 711647e7a..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 };
}
}
}
@@ -344,13 +342,13 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
? new[]
{
- nameof(Person),
- nameof(Studio),
- nameof(Year),
- nameof(MusicGenre),
- nameof(Genre)
+ BaseItemKind.Person,
+ BaseItemKind.Studio,
+ BaseItemKind.Year,
+ BaseItemKind.MusicGenre,
+ BaseItemKind.Genre
}
- : Array.Empty<string>();
+ : Array.Empty<BaseItemKind>();
var query = new InternalItemsQuery(user)
{
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index 40436d9c5..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();
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 8a9a4b865..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;
@@ -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 8577d722e..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,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(Studio) },
+ IncludeItemTypes = new[] { BaseItemKind.Studio },
IsDeadStudio = true,
IsLocked = false
});
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 644f9050d..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;
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
foreach (var virtualFolder in virtualFolders)
{
- if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
+ if (!virtualFolder.Locations.Contains(path, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -891,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)
@@ -1778,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)
@@ -2137,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)
{
@@ -2332,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();
@@ -2352,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)
{
@@ -2387,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;
@@ -2446,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;
@@ -2511,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();
@@ -2524,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,
@@ -2621,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 5726d7158..a88a1fe84 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -187,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.Replace("\"", "\\\""), // Escape quotes in filename
+ targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 1f963e4a2..dd0cb6c5d 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -9,8 +9,9 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
+using System.Net.Http.Json;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -20,7 +21,6 @@ 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;
@@ -35,7 +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 ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -43,12 +42,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
- IHttpClientFactory httpClientFactory,
- ICryptoProvider cryptoProvider)
+ IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
- _cryptoProvider = cryptoProvider;
}
/// <inheritdoc />
@@ -104,11 +101,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
};
- 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);
@@ -124,8 +120,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
- programRequestOptions.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(programIds, _jsonOptions));
- programRequestOptions.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
+ 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);
@@ -169,12 +164,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
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))
@@ -182,7 +177,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
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) ??
@@ -246,19 +241,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (programInfo.AudioProperties.Count != 0)
{
- if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase))
+ if (programInfo.AudioProperties.Contains("atmos", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Atmos;
}
- else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase))
+ else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase))
+ else if (programInfo.AudioProperties.Contains("dd", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.DolbyDigital;
}
- else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase))
+ else if (programInfo.AudioProperties.Contains("stereo", StringComparison.OrdinalIgnoreCase))
{
audioType = ProgramAudio.Stereo;
}
@@ -320,8 +315,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
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)
@@ -330,7 +325,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
.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;
}
@@ -392,9 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (details.Genres != null)
{
info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
- info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase);
+ 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;
}
@@ -403,7 +398,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info;
}
- private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> 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)))
@@ -648,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();
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 0c0ec48d9..3da9d02b8 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -190,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,
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 598e3f88a..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 },
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index a41b63f28..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
{
@@ -191,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,
@@ -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,
@@ -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();
@@ -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);
@@ -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);
}
}
@@ -1878,7 +1876,7 @@ namespace Emby.Server.Implementations.LiveTv
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+ IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
ChannelIds = channelIds,
MaxStartDate = now,
MinEndDate = now,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index f9d151a81..9ab4cc628 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -51,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);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 9fba17ca3..1f02acfa3 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;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 08b9260b9..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;
@@ -119,7 +120,7 @@ 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, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 506ef5548..708ff52d7 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -283,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[] { ' ', '.' });
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index a83a453b4..9d4d40e51 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -11,11 +11,11 @@
"Collections": "التجميعات",
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
- "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
+ "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فشلت من {0}",
"Favorites": "مفضلات",
"Folders": "المجلدات",
"Genres": "التضنيفات",
- "HeaderAlbumArtists": "ألبوم الفنان",
+ "HeaderAlbumArtists": "فناني الألبوم",
"HeaderContinueWatching": "استمر بالمشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
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/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index db3c13d80..2dee5e327 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -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",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 1ea378321..25f51db16 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -15,7 +15,7 @@
"Favorites": "Oblíbené",
"Folders": "Složky",
"Genres": "Žánry",
- "HeaderAlbumArtists": "Album umělce",
+ "HeaderAlbumArtists": "Umělci alba",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json
index 0fa72dea4..1192f5c88 100644
--- a/Emby.Server.Implementations/Localization/Core/cy.json
+++ b/Emby.Server.Implementations/Localization/Core/cy.json
@@ -10,5 +10,49 @@
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
"Artists": "Artistiaid",
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
- "Albums": "Albwmau"
+ "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/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 86ce9240e..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": "Artist's Album",
+ "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index ca127cdb8..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": "Artist's Album",
+ "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 12541a756..8abf7fa66 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -11,7 +11,7 @@
"ItemAddedWithName": "{0} aldonis al la plurmediteko",
"HeaderLiveTV": "TV-etero",
"HeaderContinueWatching": "Daŭrigi Spektadon",
- "HeaderAlbumArtists": "Albumo de artisto",
+ "HeaderAlbumArtists": "Artistoj de albumo",
"Folders": "Dosierujoj",
"DeviceOnlineWithName": "{0} estas konektita",
"Default": "Defaŭlte",
@@ -104,7 +104,7 @@
"TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
"TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
"TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
- "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn",
+ "TaskRefreshChapterImages": "Eltiri Ĉapitrajn Bildojn",
"TaskCleanCache": "Malplenigi Staplan Katalogon",
"TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
"PluginUpdatedWithName": "{0} estis ĝisdatigita",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index d3d9d2703..f8c69712e 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -15,8 +15,8 @@
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artista del álbum",
- "HeaderContinueWatching": "Continuar viendo",
+ "HeaderAlbumArtists": "Artistas del álbum",
+ "HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index a968c6dab..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,10 +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",
- "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"
+ "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
index e5405e515..8db6a0b38 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -32,9 +32,9 @@
"ValueSpecialEpisodeName": "Eriepisood - {0}",
"ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
- "UserPasswordChangedWithName": "Kasutaja {0} parooli on muudetud",
- "UserLockedOutWithName": "Kasutaja {0} on lukustatud",
- "UserDeletedWithName": "Kasutaja {0} on kustutatud",
+ "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}",
@@ -54,9 +54,9 @@
"Plugin": "Plugin",
"Playlists": "Pleilistid",
"Photos": "Fotod",
- "NotificationOptionVideoPlaybackStopped": "Video taasesitus on peatatud",
+ "NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
"NotificationOptionVideoPlayback": "Video taasesitus algas",
- "NotificationOptionUserLockedOut": "Kasutaja on lukustatud",
+ "NotificationOptionUserLockedOut": "Kasutaja lukustati",
"NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus",
"NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine",
"NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus",
@@ -66,7 +66,7 @@
"NotificationOptionNewLibraryContent": "Lisati uut sisu",
"NotificationOptionInstallationFailed": "Paigaldamine nurjus",
"NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud",
- "NotificationOptionAudioPlaybackStopped": "Heli taasesitus peatati",
+ "NotificationOptionAudioPlaybackStopped": "Heli taasesitus lõppes",
"NotificationOptionAudioPlayback": "Heli taasesitus algas",
"NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati",
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
@@ -97,12 +97,12 @@
"HeaderFavoriteArtists": "Lemmikesitajad",
"HeaderFavoriteAlbums": "Lemmikalbumid",
"HeaderContinueWatching": "Jätka vaatamist",
- "HeaderAlbumArtists": "Albumi esitaja",
+ "HeaderAlbumArtists": "Albumi esitajad",
"Genres": "Žanrid",
"Forced": "Sunnitud",
"Folders": "Kaustad",
"Favorites": "Lemmikud",
- "FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}",
+ "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus",
"DeviceOnlineWithName": "{0} on ühendatud",
"DeviceOfflineWithName": "{0} katkestas ühenduse",
"Default": "Vaikimisi",
@@ -114,5 +114,10 @@
"Artists": "Esitajad",
"Application": "Rakendus",
"AppDeviceValues": "Rakendus: {0}, seade: {1}",
- "Albums": "Albumid"
+ "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/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.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 1265b6ef5..d60955d5f 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -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",
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index d7cda61da..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": "Album od izvođača",
+ "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 85ab1511a..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": "Előadó albumai",
+ "HeaderAlbumArtists": "Album előadó(k)",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
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 5e28cf09f..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 dell'Album",
+ "HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index c689bc58a..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": "視聴を続ける",
+ "HeaderAlbumArtists": "アルバムアーティスト",
+ "HeaderContinueWatching": "続きを見る",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index d28564a7c..1b4a18deb 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -15,7 +15,7 @@
"Favorites": "Tañdaulylar",
"Folders": "Qaltalar",
"Genres": "Janrlar",
- "HeaderAlbumArtists": "Oryndauşynyñ älbomy",
+ "HeaderAlbumArtists": "Älbom oryndauşylary",
"HeaderContinueWatching": "Qaraudy jalğastyru",
"HeaderFavoriteAlbums": "Tañdauly älbomdar",
"HeaderFavoriteArtists": "Tañdauly oryndauşylar",
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/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/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 b2dcf270c..deb28970c 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -2,7 +2,7 @@
"Albums": "Album-album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi",
- "Artists": "Artis",
+ "Artists": "Artis-artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
"CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
@@ -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",
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 79f921bcb..9d512dea1 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -15,7 +15,7 @@
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artiests Album",
+ "HeaderAlbumArtists": "Album Artiesten",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
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 e8a32a13e..4fa8d2bb4 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -15,7 +15,7 @@
"Favorites": "Ulubione",
"Folders": "Foldery",
"Genres": "Gatunki",
- "HeaderAlbumArtists": "Album artysty",
+ "HeaderAlbumArtists": "Wykonawcy albumów",
"HeaderContinueWatching": "Kontynuuj odtwarzanie",
"HeaderFavoriteAlbums": "Ulubione albumy",
"HeaderFavoriteArtists": "Ulubieni wykonawcy",
@@ -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ę",
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index e3a3bfaf1..81aa996d9 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -1,5 +1,7 @@
{
"Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
- "Artists": "Artistas"
+ "Artists": "Artistas",
+ "Songs": "Shantees",
+ "Albums": "Ships"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 525a02c88..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",
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/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 066ef5587..2766dab06 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -74,7 +74,7 @@
"NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi",
- "MusicVideos": "Video muzikore",
+ "MusicVideos": "Video Muzikore",
"Music": "Muzikë",
"Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier",
@@ -96,7 +96,7 @@
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
- "HeaderAlbumArtists": "Artistët e Albumeve",
+ "HeaderAlbumArtists": "Artistët e albumeve",
"Genres": "Zhanret",
"Folders": "Skedarët",
"Favorites": "Të preferuarat",
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 6c772c6a2..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": "Artistens album",
+ "HeaderAlbumArtists": "Albumsartister",
"HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
@@ -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.",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index d6e9aa8e5..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}",
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/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index e99ed6f0b..1c7d73615 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -16,7 +16,7 @@
"HeaderFavoriteArtists": "Улюблені виконавці",
"HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
- "HeaderAlbumArtists": "Виконавці альбомів",
+ "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
"Folders": "Каталоги",
"Favorites": "Улюблені",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 548e395a9..b7ece8d5f 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -3,7 +3,7 @@
"Favorites": "Yêu Thích",
"Folders": "Thư Mục",
"Genres": "Thể Loại",
- "HeaderAlbumArtists": "Album Nghệ sĩ",
+ "HeaderAlbumArtists": "Album nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim",
@@ -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}",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index f9df62724..ac4eb644b 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -11,7 +11,7 @@
"Collections": "合集",
"DeviceOfflineWithName": "{0} 已断开",
"DeviceOnlineWithName": "{0} 已连接",
- "FailedLoginAttemptWithUserName": "来自 {0} 的失败登入",
+ "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
"Favorites": "我的最爱",
"Folders": "文件夹",
"Genres": "风格",
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 1524fcdb2..dbd70342a 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -310,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);
}
@@ -373,60 +373,75 @@ namespace Emby.Server.Implementations.Localization
public IEnumerable<LocalizationOption> GetLocalizationOptions()
{
yield return new LocalizationOption("Afrikaans", "af");
- 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 (Hong Kong)", "zh-HK");
- 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("العربية", "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("English", "en-US");
+ yield return new LocalizationOption("Ελληνικά", "el");
yield return new LocalizationOption("Esperanto", "eo");
- yield return new LocalizationOption("Estonian", "et");
- yield return new LocalizationOption("Finnish", "fi");
- 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("Icelandic", "is");
- yield return new LocalizationOption("Indonesian", "id");
- yield return new LocalizationOption("Italian", "it");
- yield return new LocalizationOption("Japanese", "ja");
- yield return new LocalizationOption("Kazakh", "kk");
- yield return new LocalizationOption("Korean", "ko");
- yield return new LocalizationOption("Latvian", "lv");
- yield return new LocalizationOption("Lithuanian", "lt-LT");
- yield return new LocalizationOption("Malay", "ms");
- yield return new LocalizationOption("Malayalam", "ml");
- yield return new LocalizationOption("Norwegian Bokmål", "nb");
- yield return new LocalizationOption("Norwegian Nynorsk", "nn");
- yield return new LocalizationOption("Persian", "fa");
- yield return new LocalizationOption("Polish", "pl");
- yield return new LocalizationOption("Portuguese", "pt");
- yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR");
- yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT");
- yield return new LocalizationOption("Romanian", "ro");
- yield return new LocalizationOption("Russian", "ru");
- yield return new LocalizationOption("Serbian", "sr");
- 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 (Latin America)", "es-419");
- 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("Tamil", "ta");
- yield return new LocalizationOption("Telugu", "te");
- yield return new LocalizationOption("Turkish", "tr");
+ 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("Ukrainian", "uk");
+ yield return new LocalizationOption("汉语 (简化字)", "zh-CN");
+ yield return new LocalizationOption("漢語 (繁体字)", "zh-TW");
+ yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
}
}
}
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index ac6606d39..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;
@@ -120,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)
{
@@ -219,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/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index b07798fa4..9481e26f7 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -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/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index 8b1cee89d..8ec9f6161 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.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;
@@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.Playlists
}
query.Recursive = true;
- query.IncludeItemTypes = new[] { "Playlist" };
+ query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
query.Parent = null;
return LibraryManager.GetItemsResult(query);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 09ea6271d..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;
@@ -143,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
{
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 059211a0b..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>
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 341d2c8e6..d10a24bbc 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1292,7 +1292,7 @@ namespace Emby.Server.Implementations.Session
{
["ItemId"] = command.ItemId,
["ItemName"] = command.ItemName,
- ["ItemType"] = command.ItemType
+ ["ItemType"] = command.ItemType.ToString()
}
};
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index bd1966623..67a9fbd3b 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -26,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>
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index fe7dc84cb..4d09dda84 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.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);
}
/// <summary>
@@ -35,9 +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;
}
}
}
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index 7b7ba5753..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>
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 8f87717f4..c2875eeb9 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -34,7 +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);
}
}
}
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 fb97a0349..79be9a89a 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -36,7 +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);
}
}
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 4d990c871..a87831294 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -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,
@@ -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 bf51c3968..33e4e5651 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -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 ef95ebf94..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;
@@ -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);
}
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/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 369e846ae..bd3e7d9e3 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -45,6 +45,11 @@ namespace Jellyfin.Api.Auth
try
{
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))
{
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/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs
index 95d07c930..98fd22430 100644
--- a/Jellyfin.Api/Controllers/ClientLogController.cs
+++ b/Jellyfin.Api/Controllers/ClientLogController.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Net.Mime;
+using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
@@ -7,7 +6,6 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.ClientLogDtos;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.ClientLog;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -38,54 +36,6 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
- /// Post event from client.
- /// </summary>
- /// <param name="clientLogEventDto">The client log dto.</param>
- /// <response code="204">Event logged.</response>
- /// <response code="403">Event logging disabled.</response>
- /// <returns>Submission status.</returns>
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult LogEvent([FromBody] ClientLogEventDto clientLogEventDto)
- {
- if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
- {
- return Forbid();
- }
-
- var (clientName, clientVersion, userId, deviceId) = GetRequestInformation();
- Log(clientLogEventDto, userId, clientName, clientVersion, deviceId);
- return NoContent();
- }
-
- /// <summary>
- /// Bulk post events from client.
- /// </summary>
- /// <param name="clientLogEventDtos">The list of client log dtos.</param>
- /// <response code="204">All events logged.</response>
- /// <response code="403">Event logging disabled.</response>
- /// <returns>Submission status.</returns>
- [HttpPost("Bulk")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult LogEvents([FromBody] ClientLogEventDto[] clientLogEventDtos)
- {
- if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
- {
- return Forbid();
- }
-
- var (clientName, clientVersion, userId, deviceId) = GetRequestInformation();
- foreach (var dto in clientLogEventDtos)
- {
- Log(dto, userId, clientName, clientVersion, deviceId);
- }
-
- return NoContent();
- }
-
- /// <summary>
/// Upload a document.
/// </summary>
/// <response code="200">Document saved.</response>
@@ -111,39 +61,20 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
}
- var (clientName, clientVersion, _, _) = GetRequestInformation();
+ var (clientName, clientVersion) = GetRequestInformation();
var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
.ConfigureAwait(false);
return Ok(new ClientLogDocumentResponseDto(fileName));
}
- private void Log(
- ClientLogEventDto dto,
- Guid userId,
- string clientName,
- string clientVersion,
- string deviceId)
- {
- _clientEventLogger.Log(new ClientLogEvent(
- dto.Timestamp,
- dto.Level,
- userId,
- clientName,
- clientVersion,
- deviceId,
- dto.Message));
- }
-
- private (string ClientName, string ClientVersion, Guid UserId, string DeviceId) GetRequestInformation()
+ 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";
- var userId = ClaimHelpers.GetUserId(HttpContext.User) ?? Guid.Empty;
- var deviceId = ClaimHelpers.GetDeviceId(HttpContext.User) ?? "unknown-device-id";
- return (clientName, clientVersion, userId, deviceId);
+ return (clientName, clientVersion);
}
}
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 2079476d0..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;
@@ -143,21 +143,24 @@ namespace Jellyfin.Api.Controllers
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/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/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 448510c06..8a6f9b8c7 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -5,8 +5,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -30,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;
@@ -39,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/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index f0d44e5cc..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>
@@ -291,8 +295,8 @@ namespace Jellyfin.Api.Controllers
{
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 0be853ca4..4eb979d83 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
{
@@ -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
@@ -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/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 99c90d19e..db72ff2f8 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -11,9 +11,7 @@ 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;
@@ -84,7 +82,7 @@ namespace Jellyfin.Api.Controllers
{
IncludeItemTypes = new[]
{
- nameof(Movie),
+ BaseItemKind.Movie,
// nameof(Trailer),
// nameof(LiveTvProgram)
},
@@ -99,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)
@@ -182,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)
@@ -224,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)
@@ -264,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/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 0ae6109bc..b41df1abb 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -9,7 +9,6 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
using Jellyfin.Extensions.Json;
-using MediaBrowser.Common.Configuration;
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/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 35921ede8..9f57a5cdb 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -3,18 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
-using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using Jellyfin.Extensions;
-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.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -30,7 +25,6 @@ namespace Jellyfin.Api.Controllers
{
private readonly IProviderManager _providerManager;
private readonly IServerApplicationPaths _applicationPaths;
- private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
/// <summary>
@@ -38,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;
}
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 3a04cb3a4..a6bbd40cc 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
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)
{
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/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 97eec4bd2..af77c801f 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -58,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/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 904738bb4..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;
@@ -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/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 5dd773331..e20bcd7a7 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -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)
@@ -153,11 +153,11 @@ namespace Jellyfin.Api.Controllers
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,
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index a33a0826c..f6fbdc302 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -6,7 +6,6 @@ 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 Jellyfin.Extensions;
@@ -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/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index e1cbc6f33..3c079a71d 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -451,7 +451,7 @@ 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);
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
if (liveStreamInfo == null)
@@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers
// 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);
@@ -484,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)
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/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
deleted file mode 100644
index 76fb27bcc..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 ArgumentException("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 4abe4c5d5..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;
@@ -462,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/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
index 61e18220a..3fa07720a 100644
--- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
+++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs
@@ -17,7 +17,6 @@ namespace Jellyfin.Api.Helpers
private readonly TranscodingJobDto? _job;
private readonly TranscodingJobHelper? _transcodingJobHelper;
private readonly int _timeoutMs;
- private int _bytesWritten;
private bool _disposed;
/// <summary>
@@ -71,53 +70,58 @@ namespace Jellyfin.Api.Helpers
/// <inheritdoc />
public override void Flush()
{
- _stream.Flush();
+ // Not supported
}
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
+ => Read(buffer.AsSpan(offset, count));
+
+ /// <inheritdoc />
+ public override int Read(Span<byte> buffer)
{
- return _stream.Read(buffer, offset, count);
+ int totalBytesRead = 0;
+ var stopwatch = Stopwatch.StartNew();
+
+ while (KeepReading(stopwatch.ElapsedMilliseconds))
+ {
+ totalBytesRead += _stream.Read(buffer);
+ if (totalBytesRead > 0)
+ {
+ break;
+ }
+
+ Thread.Sleep(50);
+ }
+
+ UpdateBytesWritten(totalBytesRead);
+
+ 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);
+
+ /// <inheritdoc />
+ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
int totalBytesRead = 0;
- int remainingBytesToRead = count;
var stopwatch = Stopwatch.StartNew();
- int newOffset = offset;
- while (remainingBytesToRead > 0)
+ while (KeepReading(stopwatch.ElapsedMilliseconds))
{
- cancellationToken.ThrowIfCancellationRequested();
- int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
-
- remainingBytesToRead -= bytesRead;
- newOffset += bytesRead;
-
- if (bytesRead > 0)
+ totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
+ if (totalBytesRead > 0)
{
- _bytesWritten += bytesRead;
- totalBytesRead += bytesRead;
-
- if (_job != null)
- {
- _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
- }
+ break;
}
- else
- {
- // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely
- if (_job?.HasExited ?? stopwatch.ElapsedMilliseconds > _timeoutMs)
- {
- break;
- }
- await Task.Delay(50, cancellationToken).ConfigureAwait(false);
- }
+ await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
+ UpdateBytesWritten(totalBytesRead);
+
return totalBytesRead;
}
@@ -159,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 0efd3443b..ca8bc0bdd 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -137,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 1b8f24c27..ed071bcd7 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -90,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)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index a3598edfa..ccd647ebe 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" />
+ <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" />
diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs
deleted file mode 100644
index 9bf9be0a4..000000000
--- a/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Api.Models.ClientLogDtos
-{
- /// <summary>
- /// The client log dto.
- /// </summary>
- public class ClientLogEventDto
- {
- /// <summary>
- /// Gets or sets the event timestamp.
- /// </summary>
- [Required]
- public DateTime Timestamp { get; set; }
-
- /// <summary>
- /// Gets or sets the log level.
- /// </summary>
- [Required]
- public LogLevel Level { get; set; }
-
- /// <summary>
- /// Gets or sets the log message.
- /// </summary>
- [Required]
- public string Message { get; set; } = string.Empty;
- }
-}
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
index fed837b85..ab67c8732 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs
@@ -134,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 0136d9f86..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;
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 58dd945c6..87233d907 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<!-- Code analysers-->
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/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index cf002dc73..58b30ad2d 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -727,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);
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 0655c9813..a55949df8 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -7,6 +7,7 @@ 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;
@@ -219,7 +220,7 @@ namespace Jellyfin.Server.Implementations.Devices
return true;
}
- return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase)
+ return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparison.OrdinalIgnoreCase)
|| !GetCapabilities(deviceId).SupportsPersistentIdentifier;
}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 73ee69424..d22757c03 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -18,14 +18,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
+ <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="6.0.0">
+ <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/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 3ab043c64..d59d36e88 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -185,9 +185,21 @@ namespace Jellyfin.Server.Implementations.Security
authInfo.IsAuthenticated = true;
authInfo.Client = key.Name;
authInfo.Token = key.AccessToken;
- authInfo.DeviceId = string.Empty;
- authInfo.Device = string.Empty;
- authInfo.Version = string.Empty;
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+ {
+ authInfo.DeviceId = string.Empty;
+ }
+
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
+ {
+ authInfo.Device = string.Empty;
+ }
+
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
+ {
+ authInfo.Version = string.Empty;
+ }
+
authInfo.IsApiKey = true;
}
}
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/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 8ca6e8d21..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;
@@ -818,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/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index e853609d6..fa98fda69 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -416,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/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 045ed6a2b..1638310fd 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -33,18 +33,17 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<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.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0" />
- <PackageReference Include="prometheus-net" Version="5.0.1" />
- <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.1" />
+ <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.3.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
- <PackageReference Include="Serilog.Sinks.Console" Version="4.0.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="Serilog.Sinks.Map" Version="1.0.2" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
</ItemGroup>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 7365c8dbc..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 =
@@ -41,17 +54,55 @@ namespace Jellyfin.Server.Migrations
.Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
.OfType<IMigrationRoutine>()
.ToArray();
+
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);
+ }
- if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
+ /// <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++)
@@ -78,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/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
index 40f871759..74f2349f5 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
@@ -9,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;
@@ -114,6 +114,7 @@ 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");
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 6e4c2280b..f40526e22 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -13,7 +13,6 @@ using Emby.Server.Implementations;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Hosting;
@@ -26,7 +25,6 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Serilog.Extensions.Logging;
-using Serilog.Filters;
using SQLitePCL;
using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -177,6 +175,7 @@ namespace Jellyfin.Server
}
PerformStaticInitialization();
+ Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory);
var appHost = new CoreAppHost(
appPaths,
@@ -598,46 +597,22 @@ namespace Jellyfin.Server
{
// Serilog.Log is used by SerilogLoggerFactory when no logger is specified
Log.Logger = new LoggerConfiguration()
- .WriteTo.Logger(lc =>
- lc.ReadFrom.Configuration(configuration)
- .Enrich.FromLogContext()
- .Enrich.WithThreadId()
- .Filter.ByExcluding(Matching.FromSource<ClientEventLogger>()))
- .WriteTo.Logger(lc =>
- lc.WriteTo.Map(
- "ClientName",
- (clientName, wt)
- => wt.File(
- Path.Combine(appPaths.LogDirectoryPath, "log_" + clientName + "_.log"),
- rollingInterval: RollingInterval.Day,
- outputTemplate: "{Message:l}{NewLine}{Exception}",
- encoding: Encoding.UTF8))
- .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>()))
+ .ReadFrom.Configuration(configuration)
+ .Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
}
catch (Exception ex)
{
Log.Logger = new LoggerConfiguration()
- .WriteTo.Logger(lc =>
- lc.WriteTo.Async(x => x.File(
- Path.Combine(appPaths.LogDirectoryPath, "log_.log"),
- rollingInterval: RollingInterval.Day,
- outputTemplate: "{Message:l}{NewLine}{Exception}",
- encoding: Encoding.UTF8))
- .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}")
- .Enrich.FromLogContext()
- .Enrich.WithThreadId())
- .WriteTo.Logger(lc =>
- lc
- .WriteTo.Map(
- "ClientName",
- (clientName, wt)
- => wt.File(
- Path.Combine(appPaths.LogDirectoryPath, "log_" + clientName + "_.log"),
- rollingInterval: RollingInterval.Day,
- outputTemplate: "{Message:l}{NewLine}{Exception}",
- encoding: Encoding.UTF8))
- .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>()))
+ .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"),
+ rollingInterval: RollingInterval.Day,
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}",
+ encoding: Encoding.UTF8))
+ .Enrich.FromLogContext()
+ .Enrich.WithThreadId()
.CreateLogger();
Log.Logger.Fatal(ex, "Failed to create/read logger configuration");
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/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index e49ab41f4..53683cdbd 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -140,12 +140,5 @@ namespace MediaBrowser.Common
/// </summary>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
void Init(IServiceCollection serviceCollection);
-
- /// <summary>
- /// Creates the instance.
- /// </summary>
- /// <param name="type">The type.</param>
- /// <returns>System.Object.</returns>
- object CreateInstance(Type type);
}
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 441f06f69..4ed44baef 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -21,7 +21,7 @@
<ItemGroup>
<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.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index abfdb41d8..d273b54fc 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -55,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);
}
@@ -86,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);
}
diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
index 82b5b4593..dea1c2f32 100644
--- a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
+++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs
@@ -1,48 +1,24 @@
using System;
using System.IO;
using System.Threading.Tasks;
-using MediaBrowser.Model.ClientLog;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.ClientEvent
{
/// <inheritdoc />
public class ClientEventLogger : IClientEventLogger
{
- private const string LogString = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level}] [{ClientName}:{ClientVersion}]: UserId: {UserId} DeviceId: {DeviceId}{NewLine}{Message}";
- private readonly ILogger<ClientEventLogger> _logger;
private readonly IServerApplicationPaths _applicationPaths;
/// <summary>
/// Initializes a new instance of the <see cref="ClientEventLogger"/> class.
/// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{ClientEventLogger}"/> interface.</param>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
- public ClientEventLogger(
- ILogger<ClientEventLogger> logger,
- IServerApplicationPaths applicationPaths)
+ public ClientEventLogger(IServerApplicationPaths applicationPaths)
{
- _logger = logger;
_applicationPaths = applicationPaths;
}
/// <inheritdoc />
- public void Log(ClientLogEvent clientLogEvent)
- {
- _logger.Log(
- LogLevel.Critical,
- LogString,
- clientLogEvent.Timestamp,
- clientLogEvent.Level.ToString(),
- clientLogEvent.ClientName,
- clientLogEvent.ClientVersion,
- clientLogEvent.UserId ?? Guid.Empty,
- clientLogEvent.DeviceId,
- Environment.NewLine,
- clientLogEvent.Message);
- }
-
- /// <inheritdoc />
public async Task<string> WriteDocumentAsync(string clientName, string clientVersion, Stream fileContents)
{
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
diff --git a/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs
index 34968d493..ad8a1bd24 100644
--- a/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs
+++ b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs
@@ -1,7 +1,5 @@
using System.IO;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.ClientLog;
namespace MediaBrowser.Controller.ClientEvent
{
@@ -11,12 +9,6 @@ namespace MediaBrowser.Controller.ClientEvent
public interface IClientEventLogger
{
/// <summary>
- /// Logs the event from the client.
- /// </summary>
- /// <param name="clientLogEvent">The client log event.</param>
- void Log(ClientLogEvent clientLogEvent);
-
- /// <summary>
/// Writes a file to the log directory.
/// </summary>
/// <param name="clientName">The client name writing the document.</param>
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
index 536668e50..9d0187c8c 100644
--- a/MediaBrowser.Controller/Entities/Audio/Audio.cs
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -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
{
@@ -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/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index f30f8ce7f..11b95b94b 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -88,7 +88,7 @@ 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 };
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index dc6fcc55a..73a25232e 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Diacritics.Extensions;
+using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities.Audio
@@ -66,7 +67,7 @@ namespace MediaBrowser.Controller.Entities.Audio
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);
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index a76ca2305..3ef2e5192 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
@@ -14,7 +13,6 @@ using System.Threading.Tasks;
using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -23,7 +21,6 @@ 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;
@@ -41,13 +38,9 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
{
- /// <summary>
- /// The trailer folder name.
- /// </summary>
- public const string TrailersFolderName = "trailers";
- public const string ThemeSongsFolderName = "theme-music";
+ private BaseItemKind? _baseItemKind;
+
public const string ThemeSongFileName = "theme";
- public const string ThemeVideosFolderName = "backdrops";
/// <summary>
/// The supported image extensions.
@@ -84,24 +77,7 @@ namespace MediaBrowser.Controller.Entities
Model.Entities.ExtraType.Scene
};
- /// <summary>
- /// The supported extra folder names and types. See <see cref="Emby.Naming.Common.NamingOptions" />.
- /// </summary>
- public static readonly Dictionary<string, ExtraType> AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
- {
- ["extras"] = MediaBrowser.Model.Entities.ExtraType.Unknown,
- ["behind the scenes"] = MediaBrowser.Model.Entities.ExtraType.BehindTheScenes,
- ["deleted scenes"] = MediaBrowser.Model.Entities.ExtraType.DeletedScene,
- ["interviews"] = MediaBrowser.Model.Entities.ExtraType.Interview,
- ["scenes"] = MediaBrowser.Model.Entities.ExtraType.Scene,
- ["samples"] = MediaBrowser.Model.Entities.ExtraType.Sample,
- ["shorts"] = MediaBrowser.Model.Entities.ExtraType.Clip,
- ["featurettes"] = MediaBrowser.Model.Entities.ExtraType.Clip
- };
-
private string _sortName;
- private Guid[] _themeSongIds;
- private Guid[] _themeVideoIds;
private string _forcedSortName;
@@ -123,40 +99,6 @@ namespace MediaBrowser.Controller.Entities
}
[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; }
[JsonIgnore]
@@ -333,13 +275,6 @@ 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;
@@ -1161,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)
@@ -1319,96 +1254,6 @@ namespace MediaBrowser.Controller.Entities
return string.Join('/', terms);
}
- /// <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
- if (LibraryManager.GetItemById(audio.Id) is Audio.Audio dbItem)
- {
- 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)
- {
- return fileSystemChildren
- .Where(child => child.IsDirectory && AllExtrasTypesFolderNames.ContainsKey(child.Name))
- .SelectMany(folder => LibraryManager
- .ResolvePaths(FileSystem.GetFiles(folder.FullName), directoryService, null, new LibraryOptions())
- .OfType<Video>()
- .Select(video =>
- {
- // Try to retrieve it from the db. If we don't find it, use the resolved version
- if (LibraryManager.GetItemById(video.Id) is Video dbItem)
- {
- video = dbItem;
- }
-
- video.ExtraType = AllExtrasTypesFolderNames[folder.Name];
- return video;
- })
- .OrderBy(video => video.Path)) // Sort them so that the list can be easily compared for changes
- .ToArray();
- }
-
public Task RefreshMetadata(CancellationToken cancellationToken)
{
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
@@ -1438,17 +1283,12 @@ 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)
{
@@ -1524,35 +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);
- }
- }
-
- if (this is IHasTrailers hasTrailers)
- {
- 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)
@@ -1562,136 +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 subOptions = new MetadataRefreshOptions(options);
-
- if (!i.ExtraType.HasValue ||
- i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
- i.OwnerId != ownerId ||
- !i.ParentId.Equals(Guid.Empty))
- {
- i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
- 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.ThemeVideoIds = newThemeVideos.OrderBy(i => i.SortName).Select(i => i.Id).ToArray();
-
- 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 tasks = extras.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))
+ if (i.OwnerId != ownerId || i.ParentId != Guid.Empty)
{
- i.ExtraType = Model.Entities.ExtraType.ThemeSong;
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
subOptions.ForceSave = true;
@@ -1702,10 +1407,9 @@ namespace MediaBrowser.Controller.Entities
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();
+ item.ExtraIds = newExtraIds;
- return themeSongsChanged;
+ return true;
}
public string GetPresentationUniqueKey()
@@ -1938,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;
}
@@ -2017,7 +1721,7 @@ namespace MediaBrowser.Controller.Entities
public BaseItemKind GetBaseItemKind()
{
- return Enum.Parse<BaseItemKind>(GetClientTypeName());
+ return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
}
/// <summary>
@@ -2107,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)
@@ -2142,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);
@@ -2348,7 +2052,7 @@ 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)
@@ -2899,14 +2603,14 @@ 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>
@@ -2946,7 +2650,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public bool Equals(BaseItem other) => 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/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index ec1ebaabe..55551e70e 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -792,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;
@@ -882,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;
@@ -1101,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;
}
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index 338f96204..4be673237 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -6,7 +6,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Diacritics.Extensions;
-using MediaBrowser.Controller.Entities.Audio;
+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/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 f4271678d..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>
@@ -42,57 +33,6 @@ namespace MediaBrowser.Controller.Entities
/// <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>
- /// <param name="item">Media item.</param>
- /// <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>
- /// <param name="item">Media item.</param>
- /// <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 0baa7725e..f06b5c787 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -27,13 +27,13 @@ namespace MediaBrowser.Controller.Entities
ExcludeArtistIds = Array.Empty<Guid>();
ExcludeInheritedTags = Array.Empty<string>();
ExcludeItemIds = Array.Empty<Guid>();
- ExcludeItemTypes = Array.Empty<string>();
+ 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<string>();
+ IncludeItemTypes = Array.Empty<BaseItemKind>();
ItemIds = Array.Empty<Guid>();
MediaTypes = Array.Empty<string>();
MinSimilarityScore = 20;
@@ -87,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; }
@@ -229,7 +229,7 @@ namespace MediaBrowser.Controller.Entities
public Guid ParentId { get; set; }
- public string? ParentType { get; set; }
+ public BaseItemKind? ParentType { get; set; }
public Guid[] AncestorIds { get; set; }
@@ -314,7 +314,7 @@ namespace MediaBrowser.Controller.Entities
else
{
ParentId = value.Id;
- ParentType = value.GetType().Name;
+ ParentType = value.GetBaseItemKind();
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index e46f99cd5..6b93d8d87 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -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.
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/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 27c3ff81b..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.
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index e4933e968..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.
@@ -131,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)
@@ -159,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;
@@ -213,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)
@@ -239,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;
@@ -259,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
};
@@ -363,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
};
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/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index 62f3c4b55..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;
@@ -102,7 +103,7 @@ 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);
}
@@ -170,12 +171,12 @@ namespace MediaBrowser.Controller.Entities
public static bool IsEligibleForGrouping(string viewType)
{
- return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ return _viewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
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)
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 266fda767..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)
@@ -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);
@@ -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,7 +196,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { nameof(Movie) };
+ query.IncludeItemTypes = new[] { BaseItemKind.Movie };
return _libraryManager.GetItemsResult(query);
}
@@ -210,7 +204,7 @@ namespace MediaBrowser.Controller.Entities
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
@@ -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
};
}
@@ -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
@@ -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);
}
@@ -498,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;
}
@@ -749,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;
@@ -767,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)
@@ -780,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)
@@ -790,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;
}
@@ -814,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;
@@ -824,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;
@@ -857,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;
}
@@ -975,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();
}
@@ -984,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();
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 7dd95b85c..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;
@@ -33,6 +34,7 @@ namespace MediaBrowser.Controller.Entities
AdditionalParts = Array.Empty<string>();
LocalAlternateVersions = Array.Empty<string>();
SubtitleFiles = Array.Empty<string>();
+ AudioFiles = Array.Empty<string>();
LinkedAlternateVersions = Array.Empty<LinkedChild>();
}
@@ -98,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>
@@ -185,7 +193,7 @@ namespace MediaBrowser.Controller.Entities
{
if (SourceType == SourceType.Channel)
{
- return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+ return !Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase);
}
return !IsActiveRecording();
@@ -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/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 7da492af3..75ec5f213 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -2,7 +2,6 @@
#pragma warning disable CS1591
-using System.Collections.Generic;
using System.Net;
using MediaBrowser.Common;
using MediaBrowser.Model.System;
@@ -43,11 +42,6 @@ namespace MediaBrowser.Controller
string FriendlyName { get; }
/// <summary>
- /// Gets the configured published server url.
- /// </summary>
- string PublishedServerUrl { get; }
-
- /// <summary>
/// Gets the system info.
/// </summary>
/// <param name="request">The HTTP request.</param>
@@ -60,25 +54,22 @@ namespace MediaBrowser.Controller
/// 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 an URL that can be used to access the API over LAN.
@@ -103,8 +94,6 @@ namespace MediaBrowser.Controller
/// <returns>The API URL.</returns>
string GetLocalApiUrl(string hostname, string scheme = null, int? port = null);
- IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo();
-
string ExpandVirtualPath(string path);
string ReverseVirtualPath(string path);
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index d40e56c7d..eba92695e 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -6,7 +6,6 @@ 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;
@@ -59,10 +58,12 @@ namespace MediaBrowser.Controller.Library
/// </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.
@@ -397,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>
@@ -441,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.
@@ -625,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/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index bfc1e4857..91d162b41 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -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>
@@ -237,6 +238,40 @@ namespace MediaBrowser.Controller.Library
}
/// <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>
+ /// Gets the file system children that do not hit the ignore file check.
+ /// </summary>
+ /// <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()
+ {
+ 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>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index 074e023e8..299773100 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;
@@ -74,7 +74,7 @@ namespace MediaBrowser.Controller.LiveTv
/// </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);
[JsonIgnore]
public bool IsRepeat { get; set; }
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 111dc0d27..6c4a5ea17 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
@@ -66,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.
@@ -80,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.
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 1996335fe..cf3b7bc7a 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,10 +14,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Diacritics" Version="3.3.4" />
+ <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.0" PrivateAssets="All" />
+ <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
</ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 5715194b8..92b345f12 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -696,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();
}
@@ -1999,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
{
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 7d62fb6e1..1418e583e 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -7,6 +7,7 @@ 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;
@@ -95,10 +96,10 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <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="outputExtension">The extension of the file to write, including the '.'.</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, string outputExtension, CancellationToken cancellationToken);
+ Task<string> ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken);
/// <summary>
/// Gets the media info.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 5e671a725..89f3bdf46 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -189,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
@@ -201,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
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/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index a42c7f8b5..90fd6e269 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -4,6 +4,7 @@
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/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.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 f79147803..de505b686 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;
@@ -117,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));
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index a2bac7b49..1c97a1982 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -19,6 +19,7 @@ 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;
@@ -478,17 +479,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
Protocol = MediaProtocol.File
};
- return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, ".jpg", 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, ".jpg", 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, string outputExtension, 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, outputExtension, cancellationToken);
+ return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, targetFormat, cancellationToken);
}
private async Task<string> ExtractImage(
@@ -500,7 +501,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool isAudio,
Video3DFormat? threedFormat,
TimeSpan? offset,
- string outputExtension,
+ ImageFormat? targetFormat,
CancellationToken cancellationToken)
{
var inputArgument = GetInputArgument(inputFile, mediaSource);
@@ -510,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// 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, outputExtension, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, true, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -523,7 +524,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, outputExtension, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, true, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -536,7 +537,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
try
{
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, outputExtension, cancellationToken).ConfigureAwait(false);
+ return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, true, false, targetFormat, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
@@ -548,24 +549,25 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
- return await ExtractImageInternal(inputArgument, container, videoStream, imageStreamIndex, threedFormat, offset, false, false, outputExtension, 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, string outputExtension, 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));
}
- if (string.IsNullOrEmpty(outputExtension))
+ var outputExtension = targetFormat switch
{
- outputExtension = ".jpg";
- }
- else if (outputExtension[0] != '.')
- {
- outputExtension = "." + outputExtension;
- }
+ 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));
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 6bb8bcdab..9f6d8e7fe 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -22,8 +22,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="BDInfo" Version="0.7.6.1" />
- <PackageReference Include="libse" Version="3.6.2" />
+ <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" />
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 9279cb220..9057a101a 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -45,6 +45,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
"AC/DC",
"Au/Ra",
+ "Bremer/McCoy",
"이달의 소녀 1/3",
"LOONA 1/3",
"LOONA / yyxy",
@@ -648,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))
{
@@ -721,15 +717,30 @@ namespace MediaBrowser.MediaEncoding.Probing
}
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;
}
@@ -1016,27 +1027,32 @@ 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)
{
return null;
}
- var parts = value.Split('/');
-
- float result;
-
- if (parts.Length == 2)
+ int index = value.IndexOf('/');
+ if (index == -1)
{
- result = float.Parse(parts[0], CultureInfo.InvariantCulture) / float.Parse(parts[1], CultureInfo.InvariantCulture);
+ // 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))
+ {
+ return result;
+ }
+
+ return null;
}
- else
+
+ if (!float.TryParse(value[..index], NumberStyles.Integer, CultureInfo.InvariantCulture, out var dividend)
+ || !float.TryParse(value[(index + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out var divisor))
{
- result = float.Parse(parts[0], CultureInfo.InvariantCulture);
+ return null;
}
- return float.IsNaN(result) ? null : result;
+ return divisor == 0f ? null : dividend / divisor;
}
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
@@ -1346,8 +1362,8 @@ namespace MediaBrowser.MediaEncoding.Probing
}
// Don't add artist/album artist name to studios, even if it's listed there
- if (info.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase)
- || info.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
+ if (info.Artists.Contains(studio, StringComparison.OrdinalIgnoreCase)
+ || info.AlbumArtists.Contains(studio, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index aae5359b1..d3ce6aa7f 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -16,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";
}
@@ -37,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; }
@@ -81,6 +82,8 @@ namespace MediaBrowser.Model.Configuration
public bool SaveSubtitlesWithMedia { get; set; }
+ public bool AutomaticallyAddToCollection { get; set; }
+
public TypeOptions[] TypeOptions { get; set; }
public TypeOptions? GetTypeOptions(string type)
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index b79d18abd..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;
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/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 e8fd18ae4..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;
@@ -209,6 +211,11 @@ namespace MediaBrowser.Model.Dlna
{
contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
}
+ 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
{
contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgCi + dlnaflags);
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/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 322cc367b..c6ce45788 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -677,8 +677,8 @@ 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);
@@ -1213,6 +1213,7 @@ namespace MediaBrowser.Model.Dlna
MediaSourceInfo item,
long maxBitrate,
MediaStream subtitleStream,
+ MediaStream audioStream,
VideoOptions options,
PlayMethod playMethod)
{
@@ -1220,7 +1221,9 @@ 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.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod);
return (false, TranscodeReason.SubtitleCodecNotSupported);
@@ -1228,8 +1231,17 @@ namespace MediaBrowser.Model.Dlna
}
bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
+ if (!result)
+ {
+ return (false, TranscodeReason.ContainerBitrateExceedsLimit);
+ }
+
+ if (audioStream.IsExternal)
+ {
+ return (false, TranscodeReason.AudioIsExternal);
+ }
- return (result, result ? null : TranscodeReason.ContainerBitrateExceedsLimit);
+ return (true, null);
}
public static SubtitleProfile GetSubtitleProfile(
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/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 ee7410632..1f7e03718 100644
--- a/MediaBrowser.Model/Entities/ImageType.cs
+++ b/MediaBrowser.Model/Entities/ImageType.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace MediaBrowser.Model.Entities
{
/// <summary>
@@ -50,7 +48,10 @@ namespace MediaBrowser.Model.Entities
/// <summary>
/// The screenshot.
/// </summary>
- [Obsolete("Screenshot image type is no longer used.")]
+ /// <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/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 85947b3de..a161b99fd 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -29,10 +29,14 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" />
+ <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="6.0.0" />
+ <PackageReference Include="System.Text.Json" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 748170a0e..3b03466e9 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -12,6 +12,15 @@ 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>
@@ -50,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" },
@@ -133,37 +87,47 @@ 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, "application/octet-stream");
@@ -173,7 +137,7 @@ namespace MediaBrowser.Model.Net
/// <param name="filename">The filename to find the MIME type of.</param>
/// <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: NotNullIfNotNullAttribute("defaultValue")]
+ [return: NotNullIfNotNull("defaultValue")]
public static string? GetMimeType(string filename, string? defaultValue = null)
{
if (filename.Length == 0)
@@ -188,29 +152,15 @@ 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 defaultValue;
@@ -231,7 +181,8 @@ namespace MediaBrowser.Model.Net
return result;
}
- return null;
+ var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
+ return string.IsNullOrEmpty(extension) ? null : "." + extension;
}
}
}
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/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/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/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.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index d2a3344be..4632e1d51 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -9,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;
@@ -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;
@@ -494,7 +495,7 @@ namespace MediaBrowser.Providers.Manager
var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList();
var current = 1;
- while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
+ while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
{
current++;
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 5e1985611..b1d73c4c4 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -457,7 +457,7 @@ namespace MediaBrowser.Providers.Manager
CancellationToken cancellationToken)
{
var eligibleImages = images
- .Where(i => i.Type == type && i.Width >= minWidth)
+ .Where(i => i.Type == type && (i.Width == null || i.Width >= minWidth))
.ToList();
if (EnableImageStub(item) && eligibleImages.Count > 0)
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 90d14a973..0af76f75a 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
@@ -679,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))
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index ca557f6d6..0385ce6a7 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -11,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;
@@ -659,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>
@@ -736,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;
}
@@ -762,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;
}
@@ -1133,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/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index 8c81b08db..b4b1895f5 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -1,7 +1,5 @@
#nullable disable
-#pragma warning disable CA1002, CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -13,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;
@@ -21,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)
@@ -63,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);
@@ -75,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);
@@ -127,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
index 9b63971a9..96d7d139a 100644
--- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -8,13 +8,16 @@ 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
{
@@ -44,15 +47,21 @@ namespace MediaBrowser.Providers.MediaInfo
"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>
- public EmbeddedImageProvider(IMediaEncoder mediaEncoder)
+ /// <param name="logger">The logger.</param>
+ public EmbeddedImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ILogger<EmbeddedImageProvider> logger)
{
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
+ _logger = logger;
}
/// <inheritdoc />
@@ -114,12 +123,17 @@ namespace MediaBrowser.Providers.MediaInfo
ImageType.Primary => _primaryImageFileNames,
ImageType.Backdrop => _backdropImageFileNames,
ImageType.Logo => _logoImageFileNames,
- _ => _primaryImageFileNames
+ _ => 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 = item.GetMediaSources(false)
- .SelectMany(source => source.MediaAttachments)
+ var attachmentStream = _mediaSourceManager.GetMediaAttachments(item.Id)
.FirstOrDefault(attachment => !string.IsNullOrEmpty(attachment.FileName)
&& imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase)));
@@ -129,7 +143,11 @@ namespace MediaBrowser.Providers.MediaInfo
}
// Fall back to EmbeddedImage streams
- var imageStreams = item.GetMediaStreams().FindAll(i => i.Type == MediaStreamType.EmbeddedImage);
+ var imageStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = item.Id,
+ Type = MediaStreamType.EmbeddedImage
+ });
if (imageStreams.Count == 0)
{
@@ -156,13 +174,21 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
+ 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, ".jpg", cancellationToken)
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, format, cancellationToken)
.ConfigureAwait(false);
return new DynamicImageResponse
{
- Format = ImageFormat.Jpg,
+ Format = format,
HasImage = true,
Path = extractedImagePath,
Protocol = MediaProtocol.File
@@ -180,10 +206,6 @@ namespace MediaBrowser.Providers.MediaInfo
extension = ".jpg";
}
- string extractedAttachmentPath =
- await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, extension, cancellationToken)
- .ConfigureAwait(false);
-
ImageFormat format = extension switch
{
".bmp" => ImageFormat.Bmp,
@@ -194,6 +216,10 @@ namespace MediaBrowser.Providers.MediaInfo
_ => ImageFormat.Jpg
};
+ string extractedAttachmentPath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, format, cancellationToken)
+ .ConfigureAwait(false);
+
return new DynamicImageResponse
{
Format = format,
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index d4b5d8655..19a435196 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -7,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;
@@ -38,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);
@@ -63,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";
@@ -97,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;
}
}
@@ -107,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;
}
@@ -177,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)
@@ -226,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 4ab15f60e..77372e063 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -44,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;
@@ -59,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ AudioResolver audioResolver)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
@@ -71,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleManager = subtitleManager;
_chapterManager = chapterManager;
_libraryManager = libraryManager;
+ _audioResolver = audioResolver;
_mediaSourceManager = mediaSourceManager;
}
@@ -214,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)
@@ -575,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/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index dd4a5f061..ba284187e 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -1,7 +1,3 @@
-#nullable disable
-
-#pragma warning disable CA1002, CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -12,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,
@@ -56,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,
@@ -74,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,
@@ -120,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)
@@ -133,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 1eacbf1e1..cce71b067 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -7,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;
@@ -66,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>();
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index d226182c0..d4bf62970 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -4,7 +4,9 @@ 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;
@@ -19,16 +21,19 @@ namespace MediaBrowser.Providers.MediaInfo
/// </summary>
public class VideoImageProvider : IDynamicImageProvider, IHasOrder
{
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger<VideoImageProvider> _logger;
/// <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(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
+ public VideoImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
{
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_logger = logger;
}
@@ -78,12 +83,18 @@ namespace MediaBrowser.Providers.MediaInfo
// 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
+ var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks > 0
? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
: TimeSpan.FromSeconds(10);
- var videoStream = item.GetDefaultVideoStream() ?? item.GetMediaStreams().FirstOrDefault(i => i.Type == MediaStreamType.Video);
+ var query = new MediaStreamQuery { ItemId = item.Id, Index = item.DefaultVideoStreamIndex };
+ var videoStream = _mediaSourceManager.GetMediaStreams(query).FirstOrDefault();
+ if (videoStream == null)
+ {
+ query.Type = MediaStreamType.Video;
+ query.Index = null;
+ videoStream = _mediaSourceManager.GetMediaStreams(query).FirstOrDefault();
+ }
if (videoStream == null)
{
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index 8baca30c6..fe9986d42 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -8,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;
@@ -44,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/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 5559b9db6..8a32cb07c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -319,6 +319,12 @@ namespace MediaBrowser.Providers.Music
{
case "name-credit":
{
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
using var subReader = reader.ReadSubtree();
return ParseArtistNameCredit(subReader);
}
@@ -355,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);
@@ -457,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)
@@ -471,7 +483,7 @@ namespace MediaBrowser.Providers.Music
{
if (reader.IsEmptyElement)
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
continue;
}
@@ -481,14 +493,14 @@ namespace MediaBrowser.Providers.Music
default:
{
- reader.Skip();
+ await reader.SkipAsync().ConfigureAwait(false);
break;
}
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
@@ -755,6 +767,12 @@ namespace MediaBrowser.Providers.Music
case "artist-credit":
{
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
using var subReader = reader.ReadSubtree();
var artist = ParseArtistCredit(subReader);
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index f67ac6ede..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 fa82089c8..60b373483 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -3,11 +3,10 @@
#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;
@@ -23,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";
@@ -52,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 2409993a2..e5753b2b5 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -8,11 +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 Jellyfin.Extensions.Json;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -31,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,
@@ -45,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());
@@ -59,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>
@@ -249,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;
}
@@ -258,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 816a882b4..12ea2d55b 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -4,15 +4,16 @@
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 Jellyfin.Extensions.Json;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -28,24 +29,22 @@ namespace MediaBrowser.Providers.Plugins.Omdb
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IApplicationHost _appHost;
private readonly JsonSerializerOptions _jsonOptions;
/// <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="appHost">IApplicationHost to use.</param>
/// <param name="configurationManager">IServerConfigurationManager to use.</param>
- public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+ 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>
@@ -68,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;
@@ -79,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, CultureInfo.InvariantCulture, out var year)
- && year >= 0)
+ if (TryParseYear(result.Year, out var year))
{
item.ProductionYear = year;
}
@@ -117,7 +115,7 @@ 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>
@@ -180,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;
@@ -191,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, CultureInfo.InvariantCulture, out var year)
- && year >= 0)
+ if (TryParseYear(result.Year, out var year))
{
item.ProductionYear = year;
}
@@ -229,7 +226,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
}
- ParseAdditionalMetadata(itemResult, result);
+ ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
return true;
}
@@ -263,6 +260,30 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return Url + "&" + 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)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ year = 0;
+ return false;
+ }
+
+ if (int.TryParse(input.AsSpan(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
+ {
+ year = result;
+ return true;
+ }
+
+ year = 0;
+ return false;
+ }
+
private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(imdbId))
@@ -295,7 +316,7 @@ 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);
+ 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);
@@ -335,37 +356,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam,
seasonId));
- var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
+ 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;
}
- /// <summary>Gets response from OMDB service as type T.</summary>
- /// <param name="httpClient">HttpClient instance to use for service call.</param>
- /// <param name="url">Http URL to use for service call.</param>
- /// <param name="cancellationToken">CancellationToken to use for service call.</param>
- /// <typeparam name="T">The first generic type parameter.</typeparam>
- /// <returns>OMDB service response as type T.</returns>
- 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);
- }
-
- /// <summary>Gets response from OMDB service.</summary>
- /// <param name="httpClient">HttpClient instance to use for service call.</param>
- /// <param name="url">Http URL to use for service call.</param>
- /// <param name="cancellationToken">CancellationToken to use for service call.</param>
- /// <returns>OMDB service response as HttpResponseMessage.</returns>
- 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))
@@ -394,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;
}
@@ -431,7 +422,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Director.Trim(),
+ Name = result.Director,
Type = PersonType.Director
};
@@ -442,7 +433,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Writer.Trim(),
+ Name = result.Writer,
Type = PersonType.Writer
};
@@ -451,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
@@ -550,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))
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/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
index 9a78a7536..dec796148 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -26,5 +26,25 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// 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
index 12b4c7ca4..52693795b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -24,7 +24,21 @@
<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>
- <br />
+ <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>
@@ -39,6 +53,47 @@
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;
@@ -51,7 +106,8 @@
cancelable: false
}));
- Dashboard.hideLoadingMsg();
+ pluginConfig = config;
+ configureImageScaling();
});
});
@@ -65,6 +121,10 @@
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);
});
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index fcaacc90d..e4a56fde9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -9,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;
@@ -279,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/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 8ac9d0cab..f50f15877 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -9,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;
@@ -188,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/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 7afaddc24..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;
@@ -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/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 77e22ffbf..f565b6569 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -9,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;
@@ -365,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 cb644c8ca..28d6f4d0c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -498,7 +498,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
return null;
}
- return _tmDbClient.GetImageUrl(size, path).ToString();
+ return _tmDbClient.GetImageUrl(size, path, true).ToString();
}
/// <summary>
@@ -508,7 +508,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The absolute URL.</returns>
public string GetPosterUrl(string posterPath)
{
- return GetUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath);
+ return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
/// <summary>
@@ -518,7 +518,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The absolute URL.</returns>
public string GetProfileUrl(string actorProfilePath)
{
- return GetUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath);
+ return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
}
/// <summary>
@@ -529,7 +529,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <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, _tmDbClient.Config.Images.PosterSizes[^1], ImageType.Primary, requestLanguage, results);
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results);
}
/// <summary>
@@ -540,7 +540,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <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, _tmDbClient.Config.Images.BackdropSizes[^1], ImageType.Backdrop, requestLanguage, results);
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results);
}
/// <summary>
@@ -551,7 +551,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <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, _tmDbClient.Config.Images.ProfileSizes[^1], ImageType.Primary, requestLanguage, results);
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results);
}
/// <summary>
@@ -562,7 +562,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <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, _tmDbClient.Config.Images.StillSizes[^1], ImageType.Primary, requestLanguage, results);
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results);
}
/// <summary>
@@ -575,16 +575,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <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)
{
+ // 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++)
{
var image = images[i];
+
results.Add(new RemoteImageInfo
{
Url = GetUrl(size, image.FilePath),
CommunityRating = image.VoteAverage,
VoteCount = image.VoteCount,
- Width = image.Width,
- Height = image.Height,
+ Width = scaleImage ? null : image.Width,
+ Height = scaleImage ? null : image.Height,
Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage),
ProviderName = TmdbUtils.ProviderName,
Type = type,
@@ -593,9 +597,51 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
}
- private Task EnsureClientConfigAsync()
+ private async Task EnsureClientConfigAsync()
+ {
+ if (!_tmDbClient.HasConfig)
+ {
+ var config = await _tmDbClient.GetConfigAsync().ConfigureAwait(false);
+ ValidatePreferences(config);
+ }
+ }
+
+ 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 />
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index c2b420c33..34019e582 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -9,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;
@@ -76,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);
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 9d223b4b6..770dc3e00 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -130,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)
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 5ce22da6a..bcf9a8366 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -784,7 +784,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "fanart":
{
- var subtree = reader.ReadSubtree();
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
+ using var subtree = reader.ReadSubtree();
if (!subtree.ReadToDescendant("thumb"))
{
break;
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 594402258..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;
@@ -1003,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/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/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index 64d19803d..a7767b3c0 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -15,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;
@@ -38,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));
@@ -77,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;
diff --git a/bump_version b/bump_version
index f615606e2..41d27f5c8 100755
--- a/bump_version
+++ b/bump_version
@@ -52,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
@@ -64,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}
@@ -104,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
@@ -121,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/jellyfin.service b/debian/jellyfin.service
index e215a8536..b86f40473 100644
--- a/debian/jellyfin.service
+++ b/debian/jellyfin.service
@@ -13,7 +13,19 @@ TimeoutSec = 15
NoNewPrivileges=true
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
-ProtectKernelModules=True
+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
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 3967a165d..708c706b5 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index bc40a8059..30615cd42 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index c1b541c59..ccfaaa5f0 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 6aa98a289..988c8f16d 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index cc9d8dc79..61a008d6a 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+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
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.spec b/fedora/jellyfin.spec
index 47dee7c13..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
@@ -45,6 +46,16 @@ 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/jellyfin.ruleset b/jellyfin.ruleset
index e14c1c427..7adc35087 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -38,14 +38,24 @@
</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" />
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index 51b955145..321cfa502 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -9,7 +9,7 @@ namespace Jellyfin.Extensions.Json.Converters
/// 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 Jellyfin.Extensions.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>();
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
index be94dd519..ea6d141cb 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
@@ -12,15 +12,19 @@ namespace Jellyfin.Extensions.Json.Converters
{
/// <inheritdoc />
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var guidStr = reader.GetString();
- return guidStr == null ? Guid.Empty : new Guid(guidStr);
- }
+ => 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)
- {
- writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
- }
+ => WriteInternal(writer, value);
+
+ internal static void WriteInternal(Utf8JsonWriter writer, Guid value)
+ => writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
}
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
index 6192d1598..b477bcb66 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs
@@ -1,5 +1,4 @@
using System;
-using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -12,21 +11,19 @@ namespace Jellyfin.Extensions.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/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
index 6de238b39..28437023f 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
@@ -15,13 +15,10 @@ namespace Jellyfin.Extensions.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 Jellyfin.Extensions.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/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs
index 1a7a8c4f5..36b36c9b4 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonStringConverter.cs
@@ -13,20 +13,11 @@ namespace Jellyfin.Extensions.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/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index cd03958b6..6f5c0ed0c 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -132,6 +132,8 @@ 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(
diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
index 4ba7e1d2f..c4640bd22 100644
--- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
+++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
@@ -55,35 +55,5 @@ namespace Jellyfin.Api.Tests.Helpers
return data;
}
-
- [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);
- }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 2aced0669..bcbe9c1be 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -15,7 +15,7 @@
<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="6.0.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" />
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.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 4504924cb..0fc8724b6 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -18,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()
{
diff --git a/tests/Jellyfin.Common.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs
index bfece97b6..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
{
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/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/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index d13e89cee..8dd637559 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -81,9 +81,7 @@ namespace Jellyfin.Naming.Tests.Video
private void Test(string input, ExtraType? expectedType)
{
- var parser = GetExtraTypeParser(_videoOptions);
-
- var extraType = parser.GetExtraInfo(input).ExtraType;
+ var extraType = ExtraResolver.GetExtraInfo(input, _videoOptions).ExtraType;
Assert.Equal(expectedType, extraType);
}
@@ -93,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.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
index b194e3885..558321810 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -5,11 +6,14 @@ 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;
@@ -17,200 +21,136 @@ namespace Jellyfin.Providers.Tests.MediaInfo
{
public class EmbeddedImageProviderTests
{
- private static TheoryData<BaseItem> GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData()
- {
- return new ()
- {
- new AudioBook(),
- new BoxSet(),
- new Series(),
- new Season(),
- };
- }
-
[Theory]
- [MemberData(nameof(GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty_TestData))]
- public void GetSupportedImages_UnsupportedBaseItems_ReturnsEmpty(BaseItem item)
+ [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)
{
- var embeddedImageProvider = GetEmbeddedImageProvider(null);
- Assert.Empty(embeddedImageProvider.GetSupportedImages(item));
- }
-
- private static TheoryData<BaseItem, IEnumerable<ImageType>> GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData()
- {
- return new TheoryData<BaseItem, IEnumerable<ImageType>>
- {
- { new Episode(), new List<ImageType> { ImageType.Primary } },
- { new Movie(), new List<ImageType> { ImageType.Logo, ImageType.Backdrop, ImageType.Primary } },
- };
- }
-
- [Theory]
- [MemberData(nameof(GetSupportedImages_SupportedBaseItems_ReturnsPopulated_TestData))]
- public void GetSupportedImages_SupportedBaseItems_ReturnsPopulated(BaseItem item, IEnumerable<ImageType> expected)
- {
- var embeddedImageProvider = GetEmbeddedImageProvider(null);
+ 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_InputWithNoStreams_ReturnsNoImage()
+ public async void GetImage_NoStreams_ReturnsNoImage()
{
- var embeddedImageProvider = GetEmbeddedImageProvider(null);
+ var input = new Movie();
- var input = GetMovie(new List<MediaAttachment>(), new List<MediaStream>());
+ 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);
}
- [Fact]
- public async void GetImage_InputWithUnlabeledAttachments_ReturnsNoImage()
+ [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 embeddedImageProvider = GetEmbeddedImageProvider(null);
-
- // add an attachment without a filename - has a list to look through but finds nothing
- var input = GetMovie(
- new List<MediaAttachment> { new () },
- new List<MediaStream>());
-
- var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
- Assert.NotNull(actual);
- Assert.False(actual.HasImage);
- }
+ 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
+ });
+ }
- [Fact]
- public async void GetImage_InputWithLabeledAttachments_ReturnsCorrectSelection()
- {
- // first tests file extension detection, second uses mimetype, third defaults to jpg
- MediaAttachment sampleAttachment1 = new () { FileName = "clearlogo.png", Index = 1 };
- MediaAttachment sampleAttachment2 = new () { FileName = "backdrop", MimeType = "image/bmp", Index = 2 };
- MediaAttachment sampleAttachment3 = new () { FileName = "poster", Index = 3 };
- string targetPath1 = "path1.png";
- string targetPath2 = "path2.bmp";
- string targetPath3 = "path2.jpg";
+ 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>(), 1, ".png", CancellationToken.None))
- .Returns(Task.FromResult(targetPath1));
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 2, ".bmp", CancellationToken.None))
- .Returns(Task.FromResult(targetPath2));
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), It.IsAny<MediaStream>(), 3, ".jpg", CancellationToken.None))
- .Returns(Task.FromResult(targetPath3));
- var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
-
- var input = GetMovie(
- new List<MediaAttachment> { sampleAttachment1, sampleAttachment2, sampleAttachment3 },
- new List<MediaStream>());
-
- var actualLogo = await embeddedImageProvider.GetImage(input, ImageType.Logo, CancellationToken.None);
- Assert.NotNull(actualLogo);
- Assert.True(actualLogo.HasImage);
- Assert.Equal(targetPath1, actualLogo.Path);
- Assert.Equal(ImageFormat.Png, actualLogo.Format);
-
- var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
- Assert.NotNull(actualBackdrop);
- Assert.True(actualBackdrop.HasImage);
- Assert.Equal(targetPath2, actualBackdrop.Path);
- Assert.Equal(ImageFormat.Bmp, actualBackdrop.Format);
-
- var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
- Assert.NotNull(actualPrimary);
- Assert.True(actualPrimary.HasImage);
- Assert.Equal(targetPath3, actualPrimary.Path);
- Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
- }
-
- [Fact]
- public async void GetImage_InputWithUnlabeledEmbeddedImages_BackdropReturnsNoImage()
- {
- var embeddedImageProvider = GetEmbeddedImageProvider(null);
+ 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 input = GetMovie(
- new List<MediaAttachment>(),
- new List<MediaStream> { new () { Type = MediaStreamType.EmbeddedImage } });
-
- var actual = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
+ var actual = await embeddedImageProvider.GetImage(input, type, CancellationToken.None);
Assert.NotNull(actual);
- Assert.False(actual.HasImage);
+ 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);
+ }
}
- [Fact]
- public async void GetImage_InputWithUnlabeledEmbeddedImages_PrimaryReturnsImage()
+ [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)
{
- MediaStream sampleStream = new () { Type = MediaStreamType.EmbeddedImage, Index = 1 };
- string targetPath = "path";
-
+ 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>(), sampleStream, 1, ".jpg", CancellationToken.None))
- .Returns(Task.FromResult(targetPath));
- var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
-
- var input = GetMovie(
- new List<MediaAttachment>(),
- new List<MediaStream> { sampleStream });
-
- var actual = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
+ 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);
- Assert.True(actual.HasImage);
- Assert.Equal(targetPath, actual.Path);
- Assert.Equal(ImageFormat.Jpg, actual.Format);
- }
-
- [Fact]
- public async void GetImage_InputWithLabeledEmbeddedImages_ReturnsCorrectSelection()
- {
- // primary is second stream to ensure it's not defaulting, backdrop is first
- MediaStream sampleStream1 = new () { Type = MediaStreamType.EmbeddedImage, Index = 1, Comment = "backdrop" };
- MediaStream sampleStream2 = new () { Type = MediaStreamType.EmbeddedImage, Index = 2, Comment = "cover" };
- string targetPath1 = "path1.jpg";
- string targetPath2 = "path2.jpg";
-
- var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream1, 1, ".jpg", CancellationToken.None))
- .Returns(Task.FromResult(targetPath1));
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), sampleStream2, 2, ".jpg", CancellationToken.None))
- .Returns(Task.FromResult(targetPath2));
- var embeddedImageProvider = GetEmbeddedImageProvider(mediaEncoder.Object);
-
- var input = GetMovie(
- new List<MediaAttachment>(),
- new List<MediaStream> { sampleStream1, sampleStream2 });
-
- var actualPrimary = await embeddedImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
- Assert.NotNull(actualPrimary);
- Assert.True(actualPrimary.HasImage);
- Assert.Equal(targetPath2, actualPrimary.Path);
- Assert.Equal(ImageFormat.Jpg, actualPrimary.Format);
-
- var actualBackdrop = await embeddedImageProvider.GetImage(input, ImageType.Backdrop, CancellationToken.None);
- Assert.NotNull(actualBackdrop);
- Assert.True(actualBackdrop.HasImage);
- Assert.Equal(targetPath1, actualBackdrop.Path);
- Assert.Equal(ImageFormat.Jpg, actualBackdrop.Format);
- }
-
- private static EmbeddedImageProvider GetEmbeddedImageProvider(IMediaEncoder? mediaEncoder)
- {
- return new EmbeddedImageProvider(mediaEncoder);
+ 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 Movie GetMovie(List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams)
+ private static IMediaSourceManager GetMediaSourceManager(BaseItem item, List<MediaAttachment> mediaAttachments, List<MediaStream> mediaStreams)
{
- // Mocking IMediaSourceManager GetMediaAttachments and GetMediaStreams instead of mocking Movie works, but
- // has concurrency problems between this and VideoImageProviderTests due to BaseItem.MediaSourceManager
- // being static
- var movie = new Mock<Movie>();
-
- movie.Setup(item => item.GetMediaSources(It.IsAny<bool>()))
- .Returns(new List<MediaSourceInfo> { new () { MediaAttachments = mediaAttachments } } );
- movie.Setup(item => item.GetMediaStreams())
+ 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 movie.Object;
+ return mediaSourceManager.Object;
}
}
}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
index c289a7112..33da277e3 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs
@@ -80,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
index 0f51a2b8f..839925dd1 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/VideoImageProviderTests.cs
@@ -2,8 +2,11 @@ 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;
@@ -16,85 +19,56 @@ namespace Jellyfin.Providers.Tests.MediaInfo
{
public class VideoImageProviderTests
{
- [Fact]
- public async void GetImage_InputIsPlaceholder_ReturnsNoImage()
+ private static TheoryData<Video> GetImage_UnsupportedInput_ReturnsNoImage_TestData()
{
- var videoImageProvider = GetVideoImageProvider(null);
-
- var input = new Movie
+ return new ()
{
- IsPlaceHolder = true
- };
-
- var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
- Assert.NotNull(actual);
- Assert.False(actual.HasImage);
- }
+ new Movie { IsPlaceHolder = true },
- [Fact]
- public async void GetImage_NoDefaultVideoStream_ReturnsNoImage()
- {
- var videoImageProvider = GetVideoImageProvider(null);
+ new Movie { DefaultVideoStreamIndex = null },
- var input = 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 }
};
-
- var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
- Assert.NotNull(actual);
- Assert.False(actual.HasImage);
}
- [Fact]
- public async void GetImage_DefaultSetButNoVideoStream_ReturnsNoImage()
+ [Theory]
+ [MemberData(nameof(GetImage_UnsupportedInput_ReturnsNoImage_TestData))]
+ public async void GetImage_UnsupportedInput_ReturnsNoImage(Video input)
{
- var videoImageProvider = GetVideoImageProvider(null);
-
- // set a default index but don't put anything there (invalid input, but provider shouldn't break)
- var input = GetMovie(0, null, new List<MediaStream>());
+ 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);
}
- [Fact]
- public async void GetImage_DefaultSetMultipleVideoStreams_ReturnsDefaultStreamImage()
+ [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)
{
- MediaStream firstStream = new () { Type = MediaStreamType.Video, Index = 0 };
- MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 1 };
- string targetPath = "path.jpg";
+ var input = new Movie { DefaultVideoStreamIndex = defaultIndex };
+ string targetPath = "path.jpg";
+ var mediaStreams = new List<MediaStream>();
var mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), firstStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
- .Returns(Task.FromResult("wrong stream called!"));
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
- .Returns(Task.FromResult(targetPath));
- var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
- var input = GetMovie(1, targetStream, new List<MediaStream> { firstStream, targetStream } );
-
- 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);
- }
+ for (int i = 0; i <= targetIndex; i++)
+ {
+ var mediaStream = new MediaStream { Type = MediaStreamType.Video, Index = i };
+ mediaStreams.Add(mediaStream);
- [Fact]
- public async void GetImage_InvalidDefaultSingleVideoStream_ReturnsFirstVideoStreamImage()
- {
- MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
- string targetPath = "path.jpg";
+ 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 mediaEncoder = new Mock<IMediaEncoder>(MockBehavior.Strict);
- mediaEncoder.Setup(encoder => encoder.ExtractVideoImage(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaSourceInfo>(), targetStream, It.IsAny<Video3DFormat?>(), It.IsAny<TimeSpan?>(), CancellationToken.None))
- .Returns(Task.FromResult(targetPath));
- var videoImageProvider = GetVideoImageProvider(mediaEncoder.Object);
+ var defaultStream = defaultIndex < mediaStreams.Count ? mediaStreams[targetIndex] : null;
+ var mediaSourceManager = GetMediaSourceManager(input, defaultStream, mediaStreams);
- // provide query results for default (empty) and all streams (populated)
- var input = GetMovie(5, null, new List<MediaStream> { targetStream });
+ var videoImageProvider = new VideoImageProvider(mediaSourceManager, mediaEncoder.Object, new NullLogger<VideoImageProvider>());
var actual = await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
Assert.NotNull(actual);
@@ -103,10 +77,19 @@ namespace Jellyfin.Providers.Tests.MediaInfo
Assert.Equal(ImageFormat.Jpg, actual.Format);
}
- [Fact]
- public async void GetImage_NoTimeSpanSet_CallsEncoderWithDefaultTime()
+ [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
@@ -115,62 +98,29 @@ namespace Jellyfin.Providers.Tests.MediaInfo
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 = GetVideoImageProvider(mediaEncoder.Object);
- var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
+ 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(10), actualTimeSpan);
+ Assert.Equal(TimeSpan.FromSeconds(expectedSeconds), actualTimeSpan);
}
- [Fact]
- public async void GetImage_TimeSpanSet_CallsEncoderWithCalculatedTime()
+ private static IMediaSourceManager GetMediaSourceManager(Video item, MediaStream? defaultStream, List<MediaStream> mediaStreams)
{
- MediaStream targetStream = new () { Type = MediaStreamType.Video, Index = 0 };
-
- 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 = GetVideoImageProvider(mediaEncoder.Object);
-
- var input = GetMovie(0, targetStream, new List<MediaStream> { targetStream });
- input.RunTimeTicks = 5000;
-
- // not testing return, just verifying what gets requested for time span
- await videoImageProvider.GetImage(input, ImageType.Primary, CancellationToken.None);
-
- Assert.Equal(TimeSpan.FromTicks(500), actualTimeSpan);
- }
-
- private static VideoImageProvider GetVideoImageProvider(IMediaEncoder? mediaEncoder)
- {
- // strict to ensure this isn't accidentally used where a prepared mock is intended
- mediaEncoder ??= new Mock<IMediaEncoder>(MockBehavior.Strict).Object;
- return new VideoImageProvider(mediaEncoder, new NullLogger<VideoImageProvider>());
- }
-
- private static Movie GetMovie(int defaultVideoStreamIndex, MediaStream? defaultStream, List<MediaStream> mediaStreams)
- {
- // Mocking IMediaSourceManager GetMediaStreams instead of mocking Movie works, but has concurrency problems
- // between this and EmbeddedImageProviderTests due to BaseItem.MediaSourceManager being static
- var movie = new Mock<Movie>
+ var defaultStreamList = new List<MediaStream>();
+ if (defaultStream != null)
{
- Object =
- {
- DefaultVideoStreamIndex = defaultVideoStreamIndex
- }
- };
+ defaultStreamList.Add(defaultStream);
+ }
- movie.Setup(item => item.GetDefaultVideoStream())
- .Returns(defaultStream!);
- movie.Setup(item => item.GetMediaStreams())
+ 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 movie.Object;
+ return mediaSourceManager.Object;
}
}
}
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/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 976afe195..09aec82b0 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs
@@ -61,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
Name = "The Big Bang Theory",
IsProgramSeries = true,
- OriginalAirDate = new DateTime(2018, 12, 6)
+ OriginalAirDate = new DateTime(2018, 12, 6, 0, 0, 0, DateTimeKind.Local)
});
data.Add(
@@ -70,7 +70,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
{
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"
});
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
index 09c4bd100..7abd2e685 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs
@@ -6,9 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
-using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Updates;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Updates;
using Moq;
using Moq.Protected;
@@ -44,7 +42,6 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
ConfigureMembers = true
});
_fixture.Inject(http);
- _fixture.Inject<IZipClient>(new ZipClient());
_installationManager = _fixture.Create<InstallationManager>();
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 4ea05397d..21131eb97 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -1,8 +1,8 @@
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;
@@ -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/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
index 4421ced72..5d7b0e874 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs
@@ -1,8 +1,7 @@
using System;
using System.Linq;
using System.Net;
-using System.Net.Http;
-using System.Net.Http.Headers;
+using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
@@ -62,9 +61,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Name = "ThisProfileDoesNotExist"
};
- using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
- content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- using var getResponse = await client.PostAsync("/Dlna/Profiles/" + NonExistentProfile, content).ConfigureAwait(false);
+ using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles/" + NonExistentProfile, deviceProfile, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
@@ -80,9 +77,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Name = "ThisProfileIsNew"
};
- using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(deviceProfile, _jsonOptions));
- content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
+ using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles", deviceProfile, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
}
@@ -120,9 +115,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Id = _newDeviceProfileId
};
- using var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(updatedProfile, _jsonOptions));
- content.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- using var getResponse = await client.PostAsync("/Dlna/Profiles", content).ConfigureAwait(false);
+ using var getResponse = await client.PostAsJsonAsync("/Dlna/Profiles", updatedProfile, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NoContent, getResponse.StatusCode);
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
index 19d8381ea..24251013c 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs
@@ -1,8 +1,7 @@
using System;
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.LibraryStructureDto;
@@ -71,9 +70,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Path = "/this/path/doesnt/exist"
};
- using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions));
- postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- var response = await client.PostAsync("Library/VirtualFolders/Paths", postContent).ConfigureAwait(false);
+ var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -90,9 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
PathInfo = new MediaPathInfo("test")
};
- using var postContent = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(data, _jsonOptions));
- postContent.Headers.ContentType = MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json);
- var response = await client.PostAsync("Library/VirtualFolders/Paths/Update", postContent).ConfigureAwait(false);
+ var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions).ConfigureAwait(false);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
index 9c0fc72f6..e72dacfe0 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs
@@ -1,7 +1,7 @@
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;
@@ -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 8866ab53c..588e25a82 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -3,8 +3,7 @@ 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;
@@ -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/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 5b884cddf..a59900b02 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -9,7 +9,7 @@
<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="6.0.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" />
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 29d7646a6..ada9034df 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -10,7 +10,7 @@
<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="6.0.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" />